ATG refund payment type:退货时更改退款支付组
结论
ATG Commerce 的标准退货流程通常会将退款分配到原订单已有的 PaymentGroup。信用卡订单发生退货时,退款一般通过原信用卡支付组处理,不会在退货过程中创建 CashPaymentGroup 并关联到原订单。
如果业务要求“信用卡付款、现金退款”,就需要定制退款处理逻辑。比较稳妥的方式是:
- 保留原订单及其支付组,不直接修改已经结算的支付结构;
- 将现金退款记为独立的退款交易或人工退款记录;
- 在退货单、退款单或扩展属性中记录现金退款金额、操作人和外部流水号;
- 根据项目的财务规则,确定是否还要向原支付组写入退款或调整记录。
直接向已提交、已结算的订单添加新支付组,可能影响金额校验、支付状态和对账。因此,仅调用 order.addPaymentGroup(...) 无法妥善实现这一需求。
为什么不能简单更换 PaymentGroup
在 ATG 中,PaymentGroup 不只是一个“退款方式”字段。它通常还参与:
- 订单金额分配;
PaymentGroupRelationship校验;- 授权、扣款和退款交易;
- 订单及支付组的状态计算;
- 支付网关对账。
原订单使用信用卡付款后,ATG 和支付网关通常会根据原支付交易执行 credit 或 refund。现金退款没有可供支付网关冲销的原现金交易,本质上是另一条资金流。
如果在退货时将新建的现金支付组直接加入原订单,还必须处理:
- 新支付组与订单的关系;
- 原信用卡支付组的金额分配;
- 订单总额与各支付组金额之和;
- 已有的授权、扣款和退款记录;
- 订单的重新校验、持久化及状态变化;
- 财务系统如何识别“原卡支付、现金退回”。
如果只创建支付组而不处理这些关系,可能导致 payment group 金额不平衡,甚至使订单无法保存。
推荐实现方式
方案一:按原支付方式退款
如果业务允许,应优先使用 ATG 标准流程,将可退款金额退回原信用卡支付组。这样可以保持支付交易、订单状态和财务对账的一致性。
具体使用哪些组件和方法,取决于项目采用的 ATG 版本、Commerce Service Center 配置及支付网关实现。实施时应沿用项目现有的 ReturnManager 和支付处理链。
方案二:将现金作为独立退款渠道
如果必须使用现金退款,不要把现金作为原订单的新付款来源。可以扩展退货流程,单独记录现金退款对象,例如:
public class CashRefundRecord {
private String orderId;
private String returnRequestId;
private double amount;
private String currencyCode;
private String operatorId;
private String externalTransactionId;
private String status;
// getters and setters
}
确认退货时,先校验退款金额,再调用项目自己的现金退款服务:
public void issueCashRefund(
Order order,
String returnRequestId,
double refundAmount,
String operatorId) throws RefundException {
if (order == null) {
throw new RefundException("Order is required");
}
if (refundAmount <= 0) {
throw new RefundException("Refund amount must be greater than zero");
}
double refundableAmount = calculateRefundableAmount(order, returnRequestId);
if (refundAmount > refundableAmount) {
throw new RefundException("Refund amount exceeds refundable amount");
}
CashRefundRecord record = new CashRefundRecord();
record.setOrderId(order.getId());
record.setReturnRequestId(returnRequestId);
record.setAmount(refundAmount);
record.setCurrencyCode(order.getPriceInfo().getCurrencyCode());
record.setOperatorId(operatorId);
record.setStatus("PENDING");
String transactionId = cashRefundService.refund(record);
record.setExternalTransactionId(transactionId);
record.setStatus("COMPLETED");
cashRefundRepository.save(record);
}
这里的 CashRefundRecord、cashRefundService、calculateRefundableAmount(...) 和 cashRefundRepository 都是项目扩展的示例,并非 ATG 标准 API。实际实现需要接入现有的退款金额计算、Repository、事务和审计机制。
方案三:定制退款支付组
如果现有系统要求所有退款渠道都用 PaymentGroup 表示,可以实现自定义退款支付组,或者扩展现金支付组。但这不能只靠增加一个对象完成,至少还需要定制:
PaymentGroupManager的创建逻辑;- 对应的支付处理器;
- 退款处理 pipeline;
PaymentGroupRelationship的生成和校验;- 订单及支付组的状态转换;
- Repository descriptor 与属性映射;
- 幂等控制、失败补偿和财务对账。
处理入口在概念上可以写成:
public interface AlternateRefundProcessor {
RefundResult refund(
Order order,
String returnRequestId,
PaymentGroup originalPaymentGroup,
String refundType,
double amount);
}
当 refundType 为 cash 时,由定制处理器登记现金支出,不能错误地向信用卡网关发起退款:
if ("cash".equals(refundType)) {
return cashRefundProcessor.refund(
order,
returnRequestId,
originalPaymentGroup,
amount);
}
return originalPaymentRefundProcessor.refund(
order,
returnRequestId,
originalPaymentGroup,
amount);
是否需要将自定义支付组持久化到原订单,取决于项目的数据模型。如果目的只是记录退款渠道,单独保存退款记录通常比修改原订单的支付组更安全。
注意事项
- 不要直接替换或删除已结算订单中的原
PaymentGroup,否则可能破坏历史支付记录。 - 使用
double表示金额只适合演示流程。生产代码应沿用项目现有的金额类型和舍入规则,例如BigDecimal或 ATG 当前价格模型采用的类型。 - 现金退款必须避免重复执行。建议使用订单号、退货请求 ID 和退款序号建立幂等约束。
- “商品退货完成”和“现金实际支付完成”需要分别记录,不能视为同一个状态。
- 部分支付网关或财务制度要求退款原路退回。在允许现金退款前,应确认支付机构、税务及内部审计规则。
- 不同 ATG 版本和定制项目使用的 Return Manager API 可能不同。实施时应以当前项目的
ReturnManager、退款 pipeline、订单 Repository 定义和支付处理器配置为准。
现金退款可以通过扩展实现,但不能简单地“新建一个 payment group 并关联原订单”。将现金退款设计为独立、可审计的退款交易,通常更符合 ATG 的订单支付结构。
备注:内容仅供参考。
