PAYATHON 2026

Iiko 如何删除已处理的外部财政化预付款?

支付阿杰

结论

DeleteExternalFiscalizedPaymentItem 不能直接删除状态为 PROCESSED 的支付项。

调用 ProcessPrepay 后,Iiko 已将预付款视为处理完成,这通常表示外部财政化已经发生。此时,它不再是可以直接移除的普通支付项。取消宴会时,应先撤销预付款或办理退款,使其退出 PROCESSED 状态,再按照当前 Iiko Front API 版本的状态规则,判断是否需要调用 DeleteExternalFiscalizedPaymentItem。

通常应按以下顺序处理:

  1. 在外部支付或财政化系统中撤销交易、办理退款,并完成必要的退票操作。
  2. 调用 UnprocessPrepay,撤销 Iiko 中预付款的处理状态。
  3. 重新获取订单和支付项。
  4. 如果支付项仍然存在,且当前状态允许删除,再调用 DeleteExternalFiscalizedPaymentItem。
  5. 最后取消宴会预订。

出现异常的原因

创建预付款时执行了以下代码:

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 并非原子事务。生产代码需要处理重复回调和部分成功的情况,保证取消流程具有幂等性。
  • 操作员凭据必须同时具备撤销预付款、修改订单和取消预订所需的权限,否则可能出现权限或约束类异常。

备注:内容仅供参考。