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
229
230
231
232
233
234
235
236
237
238
239
240
241
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.OrderFundBoundaryService;
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;
    private final OrderFundBoundaryService orderFundBoundary;
 
    public HuifuRefundService(JdbcTemplate jdbc, TransactionTemplate transactions, HuifuRefundGateway gateway, HuifuPaymentProperties huifuPaymentProperties, OrderFundBoundaryService orderFundBoundary) {
        this.jdbc = jdbc;
        this.transactions = transactions;
        this.gateway = gateway;
        this.huifuPaymentProperties = huifuPaymentProperties;
        this.orderFundBoundary = orderFundBoundary;
    }
 
    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(), prepared.orderId(), "GATEWAY_REJECTED", exception.getMessage());
                throw new BusinessException(ErrorCode.CONFLICT, "Refund request was rejected");
            }
            return detail(prepared.refundNo());
        } catch (BusinessException exception) {
            markFailed(prepared.refundNo(), prepared.orderId(), "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(), prepared.orderId(), 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, number(refund.get("orderId")), 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);
        // 支付、核销、退款、提现锁定都按订单串行化:锁住订单行之后才判断能不能退款。
        OrderFundBoundaryService.LockedOrder locked = orderFundBoundary.lock(number(order.get("id")));
        locked.requireRefundable();
        if (locked.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());
        // 退款受理成功即刻生效:在结果确定前订单不再可核销,也不参与提现汇总。
        orderFundBoundary.markRefundInProgress(number(order.get("id")));
        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());
            orderFundBoundary.markRefunded(prepared.orderId());
        });
    }
 
    /** 退款失败没有资金变动,订单回到未退款状态(可再次核销或再次退款)。 */
    private void markFailed(String refundNo, long orderId, String code, String message) {
        transactions.executeWithoutResult(status -> {
            int failed = 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);
            // 只有本事务真的把这笔退款置为失败,才把订单退回未退款状态;否则会覆盖并发的退款成功。
            if (failed == 1) orderFundBoundary.markRefundFailed(orderId);
        });
    }
 
    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(); }
    }
}