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 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 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 query(String refundNo) { Map 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 detail(String refundNo) { List> 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> 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 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> 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 response) { Map data = response.get("data") instanceof Map map ? (Map) 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(); } } }