connector-quickbooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

quickbooks-client

quickbooks-client

Motoko bindings for a curated slice of the QuickBooks Online Accounting API v3: Customer, Invoice, Payment, Item, Account. Schemas were transcribed from Intuit's official XSD. All 12 operations live in
Apis/DefaultApi.mo
:
saveCustomer
/
getCustomer
,
saveItem
/
getItem
,
saveAccount
/
getAccount
,
saveInvoice
/
getInvoice
/
sendInvoice
,
savePayment
/
getPayment
, and
query_
(trailing underscore —
query
is a reserved word in Motoko).
针对 QuickBooks Online Accounting API v3 精选子集的 Motoko 绑定: Customer、Invoice、Payment、Item、Account。这些 Schema 转录自 Intuit 官方 XSD。全部 12 个操作位于
Apis/DefaultApi.mo
:
saveCustomer
/
getCustomer
、
saveItem
/
getItem
、
saveAccount
/
getAccount
、
saveInvoice
/
getInvoice
/
sendInvoice
、
savePayment
/
getPayment
,以及
query_
(末尾带下划线——因为
query
是 Motoko 保留字)。

Trigger phrases

触发短语

Reach for this skill on any request mentioning: QuickBooks, QBO, invoice, "bill a customer", "send an invoice", record/receive a payment, customer, accounting, bookkeeping, chart of accounts, item/product/service, "sync to QuickBooks".
Not in this slice: Estimate, Bill, Vendor, PurchaseOrder, CreditMemo and the rest of the QBO entity set — the spec is hand-curated to Customer, Invoice, Payment, Item and Account. If a request needs one of the others, say so rather than mapping it onto Invoice; the spec has to be re-spun from Intuit's XSD first.
任何提及以下内容的请求都应使用此 skill:QuickBooks、QBO、invoice、 "bill a customer"、"send an invoice"、record/receive a payment、customer、 accounting、bookkeeping、chart of accounts、item/product/service、 "sync to QuickBooks"。
不在此切片范围内: Estimate、Bill、Vendor、PurchaseOrder、CreditMemo 以及 其余 QBO 实体集——该规范经过人工精选,仅涵盖 Customer、Invoice、 Payment、Item 和 Account。如果请求需要其中其他实体,应明确说明,而不是将其 映射为 Invoice;必须先根据 Intuit 的 XSD 重新生成该规范。

How QuickBooks authentication works (read before wiring)

QuickBooks 身份验证的工作方式(接入前必读)

QBO uses OAuth 2.0 Authorization Code — there is no static API key. Each end-user authorises their QuickBooks company; the app exchanges the authorization code for a short-lived Bearer access token (~1 hour) and passes it to the client at call time. On expiry the API returns HTTP 401 — surface a
#Err("auth_expired")
result and re-authenticate off-chain.
Two identifiers travel with every call:
  • realmId
    — the QuickBooks company id, obtained during the OAuth handshake. It is the first argument to every operation.
  • minorversion
    — the API minor version (default
    "75"
    ); pass
    ""
    to omit and use the account default.
The token exchange is itself an on-chain outcall to Intuit's token endpoint and needs the app's Client Secret. Two hazards (identical to the googlemail connector):
  • is_replicated = ?false
    on the exchange too
    — the token response is non-deterministic, so a replicated exchange duplicates across ~13 replicas and fails IC consensus.
  • The Client Secret leaks with exported source — Caffeine can export the app to a
    .zip
    or public GitHub repo, carrying the secret along. Scope it minimally and rotate if the source is shared.
Persist the per-user refresh/session token across upgrades (stable memory) so a redeploy doesn't force re-authentication. OAuth scope:
com.intuit.quickbooks.accounting
.
QBO 使用 OAuth 2.0 Authorization Code——不存在静态 API key。每位最终用户 都会授权其 QuickBooks company;应用将 authorization code 换取短时有效的 Bearer access token(约 1 小时),并在调用时传给 client。令牌过期后, API 返回 HTTP 401——应返回
#Err("auth_expired")
结果,并在 off-chain 重新进行身份验证。
每次调用都会携带两个标识符:
  • realmId
    ——QuickBooks 的 company id,在 OAuth handshake 期间取得。 它是每个操作的第一个参数。
  • minorversion
    ——API 次要版本(默认为
    "75"
    );传入
    ""
    可省略该参数, 并使用账号的默认版本。
令牌交换本身也是对 Intuit token endpoint 的 on-chain outcall,并且需要 应用的 Client Secret。这里有两个风险点(与 googlemail connector 相同):
  • 令牌交换也必须设置
    is_replicated = ?false
    ——令牌响应具有非确定性, 如果使用 replicated 交换,请求会在约 13 个 replica 上重复执行,并导致 IC consensus 失败。
  • 导出源代码时 Client Secret 也会泄露——Caffeine 可以将应用导出为
    .zip
    或公开 GitHub 仓库,从而一并携带该 secret。应尽量缩小其权限范围;如果 源代码被共享,则应轮换 secret。
在升级过程中(使用 stable memory)持久保存每位用户的 refresh/session token, 避免重新部署时被迫重新认证。OAuth scope:
com.intuit.quickbooks.accounting
。

Create vs. update vs. delete (important QBO semantics)

创建、更新与删除(重要的 QBO 语义)

  • Create and update BOTH use the
    save<Entity>
    (POST) operation.
    It's an update when the body carries the entity's
    Id
    and current
    SyncToken
    (fetch them first via
    get<Entity>
    ); set
    sparse = ?true
    for a partial update. Without
    Id
    /
    SyncToken
    it's a create.
  • Delete (transactions only —
    Invoice
    ,
    Payment
    ): call
    saveInvoice
    /
    savePayment
    with
    operation = ?#delete
    and a body carrying
    Id
    +
    SyncToken
    . The
    operation
    argument is optional (
    ?SaveInvoiceOperationParameter
    ), not a bare variant. Pass
    null
    for a normal create or update
    — the client then omits the query parameter entirely, which is what QBO wants; the spec says "Omit for create/update; 'delete' removes the invoice". Only
    ?#delete
    is routinely needed. Name-list entities (
    Customer
    ,
    Item
    ,
    Account
    ) are not deletable — set
    Active = ?false
    to deactivate instead (their
    save*
    ops take no
    operation
    argument).
  • Responses are wrapped:
    getCustomer
    returns
    CustomerResponse
    with a
    Customer
    field (and a
    time
    ); same shape per entity.
    query_
    returns a
    QueryResponse
    whose
    QueryResponse
    field holds arrays per entity type. The operation is
    query_
    , not
    query
    .
  • Invoice lines and Payment lines are different types.
    Invoice.Line
    is
    [Line]
    , where
    DetailType
    is required.
    Payment.Line
    is
    [PaymentLine]
    —
    {Amount, LinkedTxn, …}
    with no
    DetailType
    , which is what Intuit actually sends when a payment is applied to invoices. They were one shared type until 0.2.0, and that made every payment response fail to decode.
  • **创建和更新都使用
    save<Entity>
    (POST)操作。**如果请求体同时携带实体的
    Id
    和当前
    SyncToken
    (应先通过
    get<Entity>
    获取),则视为更新; 部分更新需要设置
    sparse = ?true
    。如果缺少
    Id
    /
    SyncToken
    ,则执行创建。
  • 删除(仅限交易类型——
    Invoice
    、
    Payment
    ):调用
    saveInvoice
    或
    savePayment
    ,传入
    operation = ?#delete
    ,并在请求体中携带
    Id
    +
    SyncToken
    。
    operation
    参数是可选的 (
    ?SaveInvoiceOperationParameter
    ),而不是裸 variant。正常创建或更新时 传入
    null
    ——client 随后会完全省略 query parameter,这正是 QBO 所要求的;规范写道:"Omit for create/update; 'delete' removes the invoice"。 通常只有
    ?#delete
    是必需的。名称列表实体 (
    Customer
    、
    Item
    、
    Account
    )不可删除——应改用
    Active = ?false
    将其停用(它们的
    save*
    操作不接受
    operation
    参数)。
  • 返回结果采用包装结构:
    getCustomer
    返回
    CustomerResponse
    ,其中包含
    Customer
    字段(以及
    time
    );每种实体均采用相同结构。
    query_
    返回
    QueryResponse
    ,其
    QueryResponse
    字段按实体类型保存数组。该操作名为
    query_
    ,而不是
    query
    。
  • Invoice 行与 Payment 行是不同类型。
    Invoice.Line
    是
    [Line]
    ,其中
    DetailType
    为必填。
    Payment.Line
    是
    [PaymentLine]
    ——即
    {Amount, LinkedTxn, …}
    ,不包含
    DetailType
    ;当付款被核销到发票时, Intuit 实际发送的正是这种结构。在 0.2.0 之前,二者共用同一种类型,这导致 每个付款响应都无法解码。

Frontend surfaces (two pages)

前端界面(两个页面)

Like any per-user OAuth connector, an app needs two surfaces:
  • An admin-gated Intuit app configuration page — the Client ID and Client Secret are canister-wide, set once by an admin; never expose the Client Secret to ordinary users.
  • A per-user OAuth 2.0 handshake page — each user connects their own QuickBooks company (yielding their
    realmId
    + token); re-prompt on HTTP 401.
与任何按用户进行 OAuth 的 connector 一样,应用需要两个界面:
  • 仅管理员可访问的 Intuit 应用配置页面——Client ID 和 Client Secret 属于整个 canister,由管理员设置一次;绝不能向普通用户 暴露 Client Secret。
  • 每位用户各自的 OAuth 2.0 handshake 页面——每位用户连接自己的 QuickBooks company(获取其
    realmId
    和 token);收到 HTTP 401 时重新 提示用户授权。

Usage

用法

mo
import { saveInvoice; getInvoice; sendInvoice; saveCustomer }
  "mo:quickbooks-client/Apis/DefaultApi";
import Invoice "mo:quickbooks-client/Models/Invoice";
import { defaultConfig } "mo:quickbooks-client/Config";

let cfg = {
  defaultConfig with
    auth          = ?#bearer "<off-chain OAuth2 access token>";
    is_replicated = ?false;  // non-replicated: required for writes; reads too
};
let realmId = "<company id from OAuth handshake>";

// Build an invoice: one sales line, $100, item "1", billed to customer "58".
// Invoice has no required fields → init {} then record-update the optionals.
// NOTE: DetailType is REQUIRED on a Line, and its variants are lowercase.
let inv = {
  Invoice.init {} with
    CustomerRef = ?{ value = "58"; name = null };
    Line = ?[ {
      DetailType          = #salesitemlinedetail;   // required; lowercase variant
      Amount              = ?100.0;
      Description         = ?"Consulting";
      SalesItemLineDetail = ?{ ItemRef = ?{ value = "1"; name = null };
                               Qty = ?1.0; UnitPrice = ?100.0; TaxCodeRef = null; ServiceDate = null };
      Id = null; LineNum = null; LinkedTxn = null;
    } ];
};
// saveInvoice(config, realmId, invoice, minorversion, operation)
// `operation` is optional: null omits it — the create/update path.
let created = await* saveInvoice(cfg, realmId, inv, "75", null);
// Email it to the customer (config, realmId, invoiceId, sendTo, minorversion):
// ignore await* sendInvoice(cfg, realmId, "<invoiceId>", "cust@example.com", "75");
mo
import { saveInvoice; getInvoice; sendInvoice; saveCustomer }
  "mo:quickbooks-client/Apis/DefaultApi";
import Invoice "mo:quickbooks-client/Models/Invoice";
import { defaultConfig } "mo:quickbooks-client/Config";

let cfg = {
  defaultConfig with
    auth          = ?#bearer "<off-chain OAuth2 access token>";
    is_replicated = ?false;  // non-replicated: required for writes; reads too
};
let realmId = "<company id from OAuth handshake>";

// Build an invoice: one sales line, $100, item "1", billed to customer "58".
// Invoice has no required fields → init {} then record-update the optionals.
// NOTE: DetailType is REQUIRED on a Line, and its variants are lowercase.
let inv = {
  Invoice.init {} with
    CustomerRef = ?{ value = "58"; name = null };
    Line = ?[ {
      DetailType          = #salesitemlinedetail;   // required; lowercase variant
      Amount              = ?100.0;
      Description         = ?"Consulting";
      SalesItemLineDetail = ?{ ItemRef = ?{ value = "1"; name = null };
                               Qty = ?1.0; UnitPrice = ?100.0; TaxCodeRef = null; ServiceDate = null };
      Id = null; LineNum = null; LinkedTxn = null;
    } ];
};
// saveInvoice(config, realmId, invoice, minorversion, operation)
// `operation` is optional: null omits it — the create/update path.
let created = await* saveInvoice(cfg, realmId, inv, "75", null);
// Email it to the customer (config, realmId, invoiceId, sendTo, minorversion):
// ignore await* sendInvoice(cfg, realmId, "<invoiceId>", "cust@example.com", "75");

Notes

注意事项

  • Use
    is_replicated = ?false
    for writes
    (
    save*
    ,
    sendInvoice
    , delete via
    operation = ?#delete
    ). These outcalls are non-idempotent and QBO's response is non-deterministic; in replicated mode every replica issues the request — creating duplicate invoices/payments and failing IC consensus. Reads (
    get*
    ,
    query_
    ) also use
    ?false
    (one node, ~13× cheaper).
  • Update needs a fresh
    SyncToken
    — always
    get<Entity>
    first, copy its
    SyncToken
    into the body, then
    save<Entity>
    . A stale token → HTTP 400 ("stale object").
  • minorversion
    defaults to
    "75"
    ; pass
    ""
    to omit.
  • QBO returns errors as an
    ErrorResponse
    whose list field is
    Fault.Error_
    — note the trailing underscore,
    Error
    being reserved — so surface
    Fault.Error_[0].Message
    to the caller; never blind-retry a write.
  • Sandbox vs production: point the client host at
    sandbox-quickbooks.api.intuit.com
    for the Intuit sandbox company during development (edit the server in
    Config
    ), and
    quickbooks.api.intuit.com
    for production.
  • 写入操作(
    save*
    、
    sendInvoice
    ,以及通过
    operation = ?#delete
    删除) 必须使用
    is_replicated = ?false
    。这些 outcall 不是幂等的,而且 QBO 的 响应具有非确定性;在 replicated 模式下,每个 replica 都会发出请求, 从而创建重复的发票或付款,并导致 IC consensus 失败。读取操作(
    get*
    、
    query_
    )也使用
    ?false
    (由单个节点执行,成本约低 13 倍)。
  • 更新需要最新的
    SyncToken
    ——始终先调用
    get<Entity>
    ,将其
    SyncToken
    复制到请求体,然后调用
    save<Entity>
    。token 过期会返回 HTTP 400("stale object")。
  • minorversion
    默认为
    "75"
    ;传入
    ""
    可省略。
  • QBO 以
    ErrorResponse
    形式返回错误,其列表字段为
    Fault.Error_
    ——注意 末尾的下划线,因为
    Error
    是保留字——因此应向调用方返回
    Fault.Error_[0].Message
    ;切勿盲目重试写入操作。
  • 沙箱与生产环境:开发期间,将 client host 指向
    sandbox-quickbooks.api.intuit.com
    ,以连接 Intuit sandbox company; 生产环境则指向
    quickbooks.api.intuit.com
    。