PAYATHON 2026

使用 PHP SDK 在 QuickBooks Online 中管理 Shopify 退款

技术老齐

结论

Shopify 退款不应以负数 Payment 写入 QuickBooks Online。Payment 表示客户付款,也就是应收款流入,不能用来记录退款支出。

常见的处理方式有两种:

  1. 已收款订单直接原路退款时,创建 RefundReceipt。这种方式操作简单,适合大多数 Shopify 退款。
  2. 原销售采用 Invoice + Payment,且需要保留完整的应收账款链路时,先创建 CreditMemo 和 Check,再创建金额为 0 的 Payment,通过 LinkedTxn 结清两者。

第二种方式会用到:

Line.LinkedTxn.TxnType = "Check"

其中,TxnId 必须是 QBO 中实际存在的 Check 交易 ID。Shopify 退款 ID 不能填在这里,单独把 TxnType 改为 "Check" 也无法表示退款。

方案一:使用 RefundReceipt

Shopify 订单已经收款,只需把款项退还给客户时,可以创建 RefundReceipt。它会同时记录销售冲减和资金流出,无须另建 Payment。

<?php

use QuickBooksOnline\API\Facades\RefundReceipt;

$refundAmount = 49.90;

$refund = RefundReceipt::create([
    "TxnDate" => "2026-10-22",

    // 所有脱敏交易使用同一个通用客户
    "CustomerRef" => [
        "value" => $genericCustomerId,
    ],

    // Shopify 退款如果从后续结算中扣除,应指向 Shopify 清算账户;
    // 如果直接从银行账户扣款,则指向相应银行账户。
    "DepositToAccountRef" => [
        "value" => $shopifyClearingAccountId,
    ],

    // 使用不包含个人信息的稳定外部编号
    "DocNumber" => "SHP-RF-a83f921e",

    "PrivateNote" => "Shopify refund token: a83f921e; payout: p_10482",

    "Line" => [
        [
            "Amount" => $refundAmount,
            "DetailType" => "SalesItemLineDetail",
            "Description" => "Shopify order refund",
            "SalesItemLineDetail" => [
                "ItemRef" => [
                    "value" => $refundItemId,
                ],
                "Qty" => 1,
                "UnitPrice" => $refundAmount,

                // TaxCodeRef 必须按公司所在地区及原交易税务处理确定,
                // 不应在通用代码中固定为某个税码。
            ],
        ],
    ],
]);

$createdRefund = $dataService->Add($refund);

if ($error = $dataService->getLastError()) {
    throw new RuntimeException(
        sprintf(
            "QBO refund creation failed: %s; %s",
            $error->getResponseBody(),
            $error->getIntuitErrorDetail()
        )
    );
}

$qboRefundReceiptId = $createdRefund->Id;

RefundReceipt 的行金额通常填写正数,交易类型本身已经表明这是一笔退款。除非当前 API 文档对具体字段另有明确要求,否则不要用负数表示资金流出。

如果退款涉及库存、折扣或税额,最好按照原交易拆分行项目,避免只记录一条总金额。只写总金额可能不会影响总账金额,但会导致税务或库存记录失真。

方案二:CreditMemo、Check 和零金额 Payment

如果原销售在 QBO 中采用 Invoice + Payment,并且需要清楚记录客户应收款、贷项与退款支票的关系,可以按以下流程处理:

  1. 为通用客户创建 CreditMemo,冲减收入和应收账款。
  2. 创建一笔 PaymentType = "Check" 的 Purchase,从银行或清算账户支出,记入 Accounts Receivable。
  3. 创建 TotalAmt = 0 的 Payment。
  4. 在该 Payment 的两条 Line 中分别关联 CreditMemo 和 Check,结清应收账款中的贷项与退款支出。

创建退款 Check 时,PHP SDK 通常使用 Purchase 作为顶层实体:

<?php

use QuickBooksOnline\API\Facades\Purchase;

$check = Purchase::create([
    "TxnDate" => "2026-10-22",
    "PaymentType" => "Check",

    // 实际发生资金流出的银行或 Shopify 清算账户
    "AccountRef" => [
        "value" => $shopifyClearingAccountId,
    ],

    "EntityRef" => [
        "value" => $genericCustomerId,
        "type" => "Customer",
    ],

    "DocNumber" => "SHP-RF-a83f921e",
    "PrivateNote" => "Refund check for sanitized Shopify refund a83f921e",

    "Line" => [
        [
            "Amount" => $refundAmount,
            "DetailType" => "AccountBasedExpenseLineDetail",
            "AccountBasedExpenseLineDetail" => [
                "AccountRef" => [
                    "value" => $accountsReceivableAccountId,
                ],
                "CustomerRef" => [
                    "value" => $genericCustomerId,
                ],
                "BillableStatus" => "NotBillable",
            ],
        ],
    ],
]);

$createdCheck = $dataService->Add($check);

if ($error = $dataService->getLastError()) {
    throw new RuntimeException($error->getResponseBody());
}

创建 CreditMemo 和 Check 后,再用 Payment 关联两者:

<?php

use QuickBooksOnline\API\Facades\Payment;

$linkingPayment = Payment::create([
    "TxnDate" => "2026-10-22",
    "CustomerRef" => [
        "value" => $genericCustomerId,
    ],
    "TotalAmt" => 0,
    "PrivateNote" => "Settle sanitized Shopify refund a83f921e",

    "Line" => [
        [
            "Amount" => $refundAmount,
            "LinkedTxn" => [
                [
                    "TxnId" => $createdCreditMemo->Id,
                    "TxnType" => "CreditMemo",
                ],
            ],
        ],
        [
            "Amount" => $refundAmount,
            "LinkedTxn" => [
                [
                    "TxnId" => $createdCheck->Id,
                    "TxnType" => "Check",
                ],
            ],
        ],
    ],
]);

$createdLink = $dataService->Add($linkingPayment);

if ($error = $dataService->getLastError()) {
    throw new RuntimeException($error->getResponseBody());
}

这段代码仅说明关联结构。不同地区的 QBO 公司文件可能采用不同的税务设置、账户类型和实体校验规则,因此需要先在 Sandbox 中确认当前公司配置能否接受这种组合。

同一笔退款不能同时使用完整金额的 RefundReceipt 和 CreditMemo + Check 流程,否则可能重复冲减收入或重复记录退款。

IPPCheckPayment 和 IPPCheckPurchase 的作用

IPPCheckPayment 和 IPPCheckPurchase 是 SDK 数据模型中的复合类型,用于描述 Check 的相关字段,不能作为独立的退款交易实体创建。

创建 Check 时,通常仍需使用 Purchase 实体,并设置:

"PaymentType" => "Check"

QBO 返回这笔交易后,可以在关联关系中这样引用:

[
    "TxnId" => $createdCheck->Id,
    "TxnType" => "Check",
]

API Explorer 在 Payment 页面中提到 Check,描述的是应收账款的结清关系,不代表普通退款应直接创建为 Payment。

Shopify 结算应使用清算账户

Shopify 经常把销售、退款、手续费和其他调整合并为一笔净额结算。若将每笔退款直接记入 QBO 银行账户,银行账面可能出现实际并不存在的单笔流水,给后续匹配带来困难。

可以建立 Shopify 清算账户,并按以下方式记账:

  • 销售收入进入 Shopify 清算账户。
  • 退款从该清算账户扣除。
  • Shopify 手续费单独记账。
  • 实际 payout 记为清算账户转入银行账户。
  • 银行下载的净额流水与这笔 payout 转账匹配。

如果 Shopify 确实从银行账户单独扣款,退款交易才直接使用该银行账户。

QBO 的银行下载记录通常不属于通过 Payment.LinkedTxn 直接关联的业务实体。集成程序先创建会计交易,实际银行流水随后由 QBO 的银行匹配或对账流程处理。

通用客户下如何追踪退款

所有退款可以共用一个 CustomerRef,但客户字段不能作为唯一的追踪依据。建议在集成数据库中保存以下映射:

shop_id
shopify_refund_id
shopify_order_id 或其脱敏令牌
shopify_payment_id
shopify_payout_id
qbo_realm_id
qbo_entity_type
qbo_transaction_id
qbo_sync_token
amount
currency
sync_status

QBO 交易中可以写入经过脱敏的稳定标识:

DocNumber: SHP-RF-a83f921e
PrivateNote: Shopify refund token a83f921e; payout p_10482

不要把客户姓名、邮箱、地址或完整的 Shopify 订单信息写入 PrivateNote。如果需要支持内部反查,可以保存不可逆摘要或内部随机令牌,并在自有数据库中维护映射。

DocNumber 可以用于查询和人工排查,但不能作为唯一的幂等保障。应先检查本地映射,再决定创建还是更新 QBO 交易。更新现有交易时,需要携带 QBO 返回的 Id 和最新 SyncToken。

选择建议

订单按即时销售导入,退款也直接退回资金时,优先使用 RefundReceipt。

订单按 Invoice + Payment 管理,并且需要完整保留应收账款结清过程时,使用 CreditMemo + Check + zero-amount Payment。LinkedTxn.TxnType = "Check" 只在这个流程中有明确含义。

无论采用哪种流程,一笔 Shopify 退款只能对应一套会计处理路径。上线前还需核对币种、税码、商品、库存、手续费和 Shopify payout 的入账方式。

备注:内容仅供参考。