使用 PHP SDK 在 QuickBooks Online 中管理 Shopify 退款
结论
Shopify 退款不应以负数 Payment 写入 QuickBooks Online。Payment 表示客户付款,也就是应收款流入,不能用来记录退款支出。
常见的处理方式有两种:
- 已收款订单直接原路退款时,创建
RefundReceipt。这种方式操作简单,适合大多数 Shopify 退款。 - 原销售采用
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,并且需要清楚记录客户应收款、贷项与退款支票的关系,可以按以下流程处理:
- 为通用客户创建
CreditMemo,冲减收入和应收账款。 - 创建一笔
PaymentType = "Check"的Purchase,从银行或清算账户支出,记入 Accounts Receivable。 - 创建
TotalAmt = 0的Payment。 - 在该
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 的入账方式。
备注:内容仅供参考。
