PAYATHON 2026

ATG refund payment type:退货时更改退款支付组

支付老李

结论

ATG Commerce 的标准退货流程通常会将退款分配到原订单已有的 PaymentGroup。信用卡订单发生退货时,退款一般通过原信用卡支付组处理,不会在退货过程中创建 CashPaymentGroup 并关联到原订单。

如果业务要求“信用卡付款、现金退款”,就需要定制退款处理逻辑。比较稳妥的方式是:

  • 保留原订单及其支付组,不直接修改已经结算的支付结构;
  • 将现金退款记为独立的退款交易或人工退款记录;
  • 在退货单、退款单或扩展属性中记录现金退款金额、操作人和外部流水号;
  • 根据项目的财务规则,确定是否还要向原支付组写入退款或调整记录。

直接向已提交、已结算的订单添加新支付组,可能影响金额校验、支付状态和对账。因此,仅调用 order.addPaymentGroup(...) 无法妥善实现这一需求。

为什么不能简单更换 PaymentGroup

在 ATG 中,PaymentGroup 不只是一个“退款方式”字段。它通常还参与:

  • 订单金额分配;
  • PaymentGroupRelationship 校验;
  • 授权、扣款和退款交易;
  • 订单及支付组的状态计算;
  • 支付网关对账。

原订单使用信用卡付款后,ATG 和支付网关通常会根据原支付交易执行 credit 或 refund。现金退款没有可供支付网关冲销的原现金交易,本质上是另一条资金流。

如果在退货时将新建的现金支付组直接加入原订单,还必须处理:

  1. 新支付组与订单的关系;
  2. 原信用卡支付组的金额分配;
  3. 订单总额与各支付组金额之和;
  4. 已有的授权、扣款和退款记录;
  5. 订单的重新校验、持久化及状态变化;
  6. 财务系统如何识别“原卡支付、现金退回”。

如果只创建支付组而不处理这些关系,可能导致 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 的订单支付结构。

备注:内容仅供参考。