yeg
2026-09-12 d879291f56eb64adffa44f763238ea736b4be99b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
package com.talentcard.server.business.payment;
 
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.MoneyUtils;
import com.talentcard.server.business.order.OrderRefundRequest;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
import com.talentcard.server.business.payment.huifu.HuifuRefundCommand;
import com.talentcard.server.business.payment.huifu.HuifuRefundGateway;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
 
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
@Service
public class HuifuRefundService {
 
    private static final String PROVIDER = "HUIFU";
    private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
 
    private final JdbcTemplate jdbc;
    private final TransactionTemplate transactions;
    private final HuifuRefundGateway gateway;
    private final HuifuPaymentProperties huifuPaymentProperties;
 
    public HuifuRefundService(JdbcTemplate jdbc, TransactionTemplate transactions, HuifuRefundGateway gateway, HuifuPaymentProperties huifuPaymentProperties) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
        this.huifuPaymentProperties = huifuPaymentProperties;
    }
 
    public Map<String, Object> request(String orderNo, OrderRefundRequest request) {
        PreparedRefund prepared = transactions.execute(status -> prepare(orderNo, request));
        if (prepared == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Refund initialization failed");
 
        Map<String, Object> response;
        try {
            response = gateway.createRefund(new HuifuRefundCommand(
                    prepared.refundNo(), prepared.huifuId(), prepared.paymentNo(), prepared.originalProviderNo(),
                    prepared.originalRequestDate(), prepared.refundAmount()));
        } catch (HuifuPaymentGatewayException exception) {
            if (!exception.isOutcomeUnknown()) {
                markFailed(prepared.refundNo(), "GATEWAY_REJECTED", exception.getMessage());
                throw new BusinessException(ErrorCode.CONFLICT, "Refund request was rejected");
            }
            return detail(prepared.refundNo());
        } catch (BusinessException exception) {
            markFailed(prepared.refundNo(), "LOCAL_ERROR", exception.getMessage());
            throw exception;
        } catch (RuntimeException exception) {
            return detail(prepared.refundNo());
        }
 
        ProviderResult result = ProviderResult.from(response);
        if (result.success()) {
            markSucceeded(prepared, result);
        } else if (result.failed()) {
            markFailed(prepared.refundNo(), result.code(), result.message());
            throw new BusinessException(ErrorCode.CONFLICT, "Refund request was rejected");
        } else {
            updateProviderResult(prepared.refundNo(), result);
        }
        return detail(prepared.refundNo());
    }
 
    public Map<String, Object> query(String refundNo) {
        Map<String, Object> refund = detail(refundNo);
        if (number(refund.get("refundStatus")) != 1) return refund;
        HuifuRefundCommand command = new HuifuRefundCommand(
                refundNo,
                refundHuifuId(refund.get("providerHuifuId")),
                text(refund.get("providerOriginalReqSeqId")),
                text(refund.get("providerOriginalNo")),
                text(refund.get("providerOriginalReqDate")),
                number(refund.get("refundAmount")));
        try {
            ProviderResult result = ProviderResult.from(gateway.queryRefund(command, text(refund.get("providerRefundNo"))));
            if (result.success()) markSucceeded(new PreparedRefund(refundNo, number(refund.get("orderId")), number(refund.get("refundAmount"))), result);
            else if (result.failed()) markFailed(refundNo, result.code(), result.message());
            else updateProviderResult(refundNo, result);
        } catch (HuifuPaymentGatewayException ignored) {
            // Keep the refund pending when the provider outcome is unknown.
        }
        return detail(refundNo);
    }
 
    public Map<String, Object> detail(String refundNo) {
        List<Map<String, Object>> rows = rows("""
                select r.id, r.order_id, r.refund_no, r.order_no, r.order_type, r.paid_amount, r.refund_amount,
                       r.refundable_amount, r.refund_status, r.provider, r.provider_refund_no, r.provider_huifu_id,
                       r.provider_original_no, r.provider_original_req_seq_id, r.provider_original_req_date,
                       r.provider_response_code, r.provider_response_message, r.reason, r.operator_name, r.refund_time
                from order_refund r
                where r.deleted = 0 and r.refund_no = ?
                """, refundNo);
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "Refund record not found");
        return rows.get(0);
    }
 
    private PreparedRefund prepare(String orderNo, OrderRefundRequest request) {
        List<Map<String, Object>> orders = rows("""
                select o.id, o.order_no, o.talent_id, o.card_id, o.benefit_id, o.merchant_id, o.order_type,
                       o.pay_amount, o.order_status, o.verify_status,
                       p.payment_no, p.provider_transaction_no, p.created_at as payment_created_at,
                       coalesce(p.provider_huifu_id, o.collect_huifu_id) as huifu_id
                from benefit_order o
                left join payment_transaction p on p.order_id = o.id and p.provider = ? and p.status = 'SUCCEEDED'
                where o.deleted = 0 and o.order_no = ?
                order by p.id desc
                limit 1 for update
                """, PROVIDER, orderNo);
        if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "Order not found");
        Map<String, Object> order = orders.get(0);
        if (number(order.get("orderStatus")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "Order cannot be refunded in its current state");
        if (number(order.get("verifyStatus")) == 1 && !"STORE_CONSUME".equals(text(order.get("orderType")))) {
            throw new BusinessException(ErrorCode.CONFLICT, "Verified orders cannot be refunded");
        }
 
        Integer active = jdbc.queryForObject("select count(*) from order_refund where deleted = 0 and order_id = ? and refund_status in (1,2)", Integer.class, order.get("id"));
        if (active != null && active > 0) throw new BusinessException(ErrorCode.CONFLICT, "Refund has already been submitted for this order");
 
        long paidAmount = number(order.get("payAmount"));
        long successfulRefund = refundedAmount(number(order.get("id")));
        long refundableAmount = Math.max(paidAmount - successfulRefund, 0);
        long requestedAmount = MoneyUtils.yuanToCent(request.refundAmount());
        long refundAmount = requestedAmount > 0 ? requestedAmount : refundableAmount;
        if (refundAmount <= 0 || refundAmount > refundableAmount) throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid refund amount");
 
        String paymentNo = text(order.get("paymentNo"));
        String providerNo = text(order.get("providerTransactionNo"));
        if (paymentNo.isBlank() && providerNo.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "Payment transaction is not ready for refund");
        String huifuId = refundHuifuId(order.get("huifuId"));
        String refundNo = BusinessIdGenerator.next("R");
        String requestDate = requestDate(order.get("paymentCreatedAt"));
        jdbc.update("""
                insert into order_refund(refund_no, order_id, order_no, talent_id, card_id, benefit_id, merchant_id,
                                         order_type, paid_amount, refund_amount, refundable_amount, refund_status,
                                         provider, provider_huifu_id, provider_original_no, provider_original_req_seq_id, provider_original_req_date,
                                         reason, operator_name)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
                """,
                refundNo, order.get("id"), order.get("orderNo"), order.get("talentId"), order.get("cardId"),
                order.get("benefitId"), order.get("merchantId"), order.get("orderType"), paidAmount, refundAmount,
                refundableAmount - refundAmount, PROVIDER, huifuId, providerNo, paymentNo, requestDate, request.reason(), request.operatorName());
        return new PreparedRefund(refundNo, number(order.get("id")), huifuId, paymentNo, providerNo, requestDate, refundAmount);
    }
 
    private void markSucceeded(PreparedRefund prepared, ProviderResult result) {
        transactions.executeWithoutResult(status -> {
            jdbc.update("""
                    update order_refund
                    set refund_status = 2, provider_refund_no = ?, provider_response_code = ?, provider_response_message = ?, refund_time = now()
                    where refund_no = ? and refund_status = 1
                    """, result.providerNo(), result.code(), result.message(), prepared.refundNo());
            jdbc.update("""
                    update benefit_order o
                    set o.order_status = 2
                    where o.id = ? and o.order_status = 1
                      and o.pay_amount <= (select coalesce(sum(r.refund_amount), 0) from order_refund r where r.order_id = o.id and r.refund_status = 2 and r.deleted = 0)
                    """, prepared.orderId());
        });
    }
 
    private void markFailed(String refundNo, String code, String message) {
        jdbc.update("update order_refund set refund_status = 3, provider_response_code = ?, provider_response_message = ? where refund_no = ? and refund_status = 1", code, message, refundNo);
    }
 
    private void updateProviderResult(String refundNo, ProviderResult result) {
        jdbc.update("update order_refund set provider_refund_no = coalesce(?, provider_refund_no), provider_response_code = ?, provider_response_message = ? where refund_no = ? and refund_status = 1", result.providerNo(), result.code(), result.message(), refundNo);
    }
 
    private long refundedAmount(long orderId) {
        Long total = jdbc.queryForObject("select coalesce(sum(refund_amount), 0) from order_refund where deleted = 0 and refund_status = 2 and order_id = ?", Long.class, orderId);
        return total == null ? 0 : total;
    }
 
    /**
     * The merchant a refund must be requested from: the sub-merchant fixed on the payment or the
     * order. Payments made before the collecting sub-merchant was recorded were collected by the
     * configured platform merchant, so those fall back to it instead of the refund failing.
     */
    private String refundHuifuId(Object storedHuifuId) {
        String resolved = text(storedHuifuId);
        return resolved.isBlank() ? text(huifuPaymentProperties.getMerchantId()) : resolved;
    }
 
    private String requestDate(Object value) {
        if (value instanceof LocalDateTime dateTime) return REQUEST_DATE.format(dateTime);
        if (value instanceof LocalDate date) return date.format(DateTimeFormatter.BASIC_ISO_DATE);
        return REQUEST_DATE.format(LocalDateTime.now());
    }
 
    private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
    private long number(Object value) { return value instanceof Number number ? number.longValue() : 0; }
    private String text(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
 
    private record PreparedRefund(String refundNo, long orderId, String huifuId, String paymentNo, String originalProviderNo, String originalRequestDate, long refundAmount) {
        private PreparedRefund(String refundNo, long orderId, long refundAmount) { this(refundNo, orderId, "", "", "", "", refundAmount); }
    }
 
    private record ProviderResult(boolean success, boolean failed, String code, String message, String providerNo) {
        private static ProviderResult from(Map<String, Object> response) {
            Map<String, Object> data = response.get("data") instanceof Map<?, ?> map ? (Map<String, Object>) map : response;
            String status = textValue(data.get("trans_stat"));
            String code = textValue(data.get("sub_resp_code"));
            if (code.isBlank()) code = textValue(data.get("resp_code"));
            String message = textValue(data.get("sub_resp_desc"));
            if (message.isBlank()) message = textValue(data.get("resp_desc"));
            String providerNo = textValue(data.get("hf_seq_id"));
            boolean codeSuccess = "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code);
            boolean success = codeSuccess && (status.isBlank() || "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status) || "PAID".equalsIgnoreCase(status));
            boolean failed = !success && !("P".equalsIgnoreCase(status) || "PROCESSING".equalsIgnoreCase(status) || "PENDING".equalsIgnoreCase(status));
            return new ProviderResult(success, failed, code, message, providerNo);
        }
        private static String textValue(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
    }
}