Iiko 如何删除已处理的外部财政化预付款?
支付阿杰
结论
DeleteExternalFiscalizedPaymentItem 不能直接删除状态为 PROCESSED 的支付项。
调用 ProcessPrepay 后,Iiko 已将预付款视为处理完成,这通常表示外部财政化已经发生。此时,它不再是可以直接移除的普通支付项。取消宴会时,应先撤销预付款或办理退款,使其退出 PROCESSED 状态,再按照当前 Iiko Front API 版本的状态规则,判断是否需要调用 DeleteExternalFiscalizedPaymentItem。
通常应按以下顺序处理:
- 在外部支付或财政化系统中撤销交易、办理退款,并完成必要的退票操作。
- 调用
UnprocessPrepay,撤销 Iiko 中预付款的处理状态。 - 重新获取订单和支付项。
- 如果支付项仍然存在,且当前状态允许删除,再调用
DeleteExternalFiscalizedPaymentItem。 - 最后取消宴会预订。
出现异常的原因
创建预付款时执行了以下代码:
var paymentItem = PluginContext.Operations.AddExternalFiscalizedPaymentItem(
prepaidAmount,
null,
paymentType,
order,
credentials
);
PluginContext.Operations.ProcessPrepay(credentials, order, paymentItem);
第一步只是向订单中添加 external fiscalized payment。此时支付项通常仍处于未处理状态,可以删除。
第二步调用 ProcessPrepay 后,支付项会转为已处理的预付款,状态变为 PROCESSED。这表示该笔预付款已经进入财务或财政化流程。为避免删除已经产生财务记录的交易,Iiko 会拒绝该操作并抛出以下异常:
ConstraintViolationException: Cannot delete payment item in status PROCESSED
CancelReserve 只会取消预订,不会自动撤销已经处理的预付款。
正确的取消流程
取消时应先撤销预付款,再删除支付项:
private void CancelBanquet(IikoPrepaidBookingRq rq, string callBackUrl)
{
if (string.IsNullOrWhiteSpace(rq.BanquetId) ||
!Guid.TryParse(rq.BanquetId, out var banquetId))
{
operationProvider.SendPrepaidBanquetId(
new IikoPrepaidBookingRp
{
State = "Error",
Message = "BanquetId is empty or invalid"
},
callBackUrl);
return;
}
var userBanquet = PluginContext.Operations
.GetReserves()
.SingleOrDefault(x => x.Id == banquetId);
if (userBanquet is null)
{
operationProvider.SendPrepaidBanquetId(
new IikoPrepaidBookingRp
{
State = "NotFound",
Message = "userBanquet not found"
},
callBackUrl);
return;
}
var credentials = PluginContext.Operations.GetCredentials(
AppDataV2.PaymentProcessorUserPin);
var order = userBanquet.Order;
var prepaidPayment = order.Payments.FirstOrDefault(p => p.IsPrepay);
if (prepaidPayment != null)
{
// 在此之前,应先在外部支付/财政化系统中完成撤销或退款。
PluginContext.Operations.UnprocessPrepay(
credentials,
order,
prepaidPayment);
// UnprocessPrepay 可能改变订单对象及支付项状态,
// 后续操作应尽量使用重新获取的实体。
userBanquet = PluginContext.Operations
.GetReserves()
.Single(x => x.Id == banquetId);
order = userBanquet.Order;
prepaidPayment = order.Payments.FirstOrDefault(p => p.IsPrepay);
if (prepaidPayment != null)
{
PluginContext.Operations.DeleteExternalFiscalizedPaymentItem(
prepaidPayment,
order,
credentials);
}
}
PluginContext.Operations.CancelReserve(
credentials,
userBanquet,
ReserveCancelReason.ClientRefused);
operationProvider.SendPrepaidBanquetId(
new IikoPrepaidBookingRp
{
State = "Success"
},
callBackUrl);
}
版本兼容性说明
上述示例以当前使用的 Iiko Front API 提供 UnprocessPrepay 为前提。不同 API 版本对撤销后的支付项状态,以及是否还需要调用 DeleteExternalFiscalizedPaymentItem,可能有不同规定:
- 如果调用
UnprocessPrepay后,支付项仍然存在并进入可删除状态,可以继续调用删除方法。 - 如果撤销操作已经移除了支付项,就不应再次删除。
- 如果当前版本没有
UnprocessPrepay,应改用该版本提供的预付款退款或反处理 API,不能通过修改对象或重复调用DeleteExternalFiscalizedPaymentItem来绕过状态限制。 - 如果撤销后支付项进入类似
RETURNED的终态,且仍不允许删除,就应保留该记录。财务记录通常需要通过冲正保留审计轨迹,而不是进行物理删除。
实际代码应在撤销后重新读取订单,再根据该版本 API 提供的支付状态决定下一步操作,不要继续使用旧的 prepaidPayment 引用。
注意事项
FirstOrDefault(p => p.IsPrepay)可能取到错误的预付款。一个订单可能有多笔预付款、退款记录或不同的支付类型。最好保存创建时返回的 payment item 标识,之后通过该标识定位。- external fiscalized payment 表示财政化由外部系统负责。Iiko 中的反处理不等于外部退款,也不会自动生成外部退票。更安全的顺序是先完成外部撤销,再更新 Iiko 状态。
- 如果外部退款成功,但 Iiko 撤销失败,应记录交易号、支付项 ID 和异常,并转入补偿或人工处理流程,避免重复退款。
- 不要先调用
CancelReserve,再处理预付款。预订或关联订单进入取消状态后,支付操作可能受到更多限制。 UnprocessPrepay和DeleteExternalFiscalizedPaymentItem并非原子事务。生产代码需要处理重复回调和部分成功的情况,保证取消流程具有幂等性。- 操作员凭据必须同时具备撤销预付款、修改订单和取消预订所需的权限,否则可能出现权限或约束类异常。
备注:内容仅供参考。
