@happyvertical/accounting 0.98.0 → 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,88 @@
2
2
 
3
3
  Provider-neutral accounting synchronization with Stripe billing support.
4
4
 
5
+ ## QuickBooks invoice creation and retries
6
+
7
+ Prepare a caller-owned request descriptor **once**, persist it with the approved
8
+ invoice in your durable outbox, then pass it on every create attempt:
9
+
10
+ ```ts
11
+ import { prepareQuickBooksInvoiceRequest, QuickBooksWriteError } from '@happyvertical/accounting';
12
+
13
+ const quickbooksRequest = prepareQuickBooksInvoiceRequest(invoice, {
14
+ requestId: outbox.requestId, // e.g. a persisted UUID, never regenerated on retry
15
+ realmId: outbox.realmId,
16
+ environment: 'sandbox',
17
+ });
18
+ // Persist quickbooksRequest AND the invoice snapshot before invoking push.
19
+ try {
20
+ const result = await qbo.invoices.push({ ...invoice, quickbooksRequest });
21
+ // Persist result.externalId on the same outbox record.
22
+ } catch (error) {
23
+ if (error instanceof QuickBooksWriteError && error.outcome === 'unknown') {
24
+ // Keep this record pending reconciliation; it may already exist remotely.
25
+ // Retry only the unchanged snapshot + descriptor, never with a new ID.
26
+ } else {
27
+ throw error;
28
+ }
29
+ }
30
+ ```
31
+
32
+ The helper performs no HTTP. Its JSON-roundtrippable descriptor binds the exact
33
+ mapped QuickBooks payload (SHA-256) to the realm and sandbox/production environment.
34
+ `push()` and create-only `sync()` verify that binding before authentication or
35
+ HTTP, including on a new provider instance. Restore invoice date fields as `Date`
36
+ objects when loading an outbox. Mapping uses local calendar dates, so keep the
37
+ same timezone across workers; a changed mapped date fails closed. Changes to
38
+ mapped fields, realm, environment, or descriptor hash are rejected locally.
39
+ Request IDs accept this SDK's conservative subset: 1–50 ASCII letters, digits,
40
+ dots, underscores, and hyphens. The URL receives the caller's exact `requestid`.
41
+
42
+ The descriptor is a consistency check, **not an authorization token or durable
43
+ ID registry**. The caller must enforce uniqueness, tenant/realm ownership,
44
+ immutability, and concurrency in its outbox. Never prepare a new descriptor with
45
+ an old ID for different contents: a stateless SDK cannot detect that across
46
+ processes. Existing Stripe `idempotencyKey` is not a QuickBooks request ID.
47
+ Descriptors are create-only and are refused with `externalId`; update, send,
48
+ void, customer, vendor, and bill operations do not accept them.
49
+
50
+ QuickBooks reads retain automatic retry of transport failures, HTTP 429, and
51
+ 5xx responses. Invoice creates with a descriptor retry these failures using
52
+ identical URL/body bytes; malformed successful invoice responses also remain
53
+ uncertain and may be retried under that identity. `maxRetries: 0` disables
54
+ retries; the default is three retries after the initial attempt. Timeout covers
55
+ reading the response as well as fetching it.
56
+
57
+ **All unkeyed QuickBooks writes now make one attempt**, including invoice
58
+ creates/updates/send/void and customer/vendor/bill creates and updates. This deliberately replaces the previous automatic write retry
59
+ behavior, which could duplicate a remotely accepted write. HTTP 429 is also
60
+ single-attempt for those operations. Stripe behavior is unchanged.
61
+ `QuickBooksWriteError` reports `outcome: 'rejected'` for an unkeyed HTTP client
62
+ rejection (including 429), or `outcome: 'unknown'` for transport failures,
63
+ server failures, or malformed success responses. **Every keyed failure is
64
+ conservatively `unknown`, including HTTP 4xx on the first locally observed
65
+ attempt.** A stateless SDK cannot prove that a persisted identity was never
66
+ sent by an earlier worker. This preserves uncertainty across process restarts
67
+ and later rejections without requiring mutable descriptor state. Client errors
68
+ still receive no automatic retry (except keyed 429 responses). Its realm, environment,
69
+ endpoint, optional request ID/payload hash, HTTP status, and cause support caller
70
+ reconciliation. A local validation or token acquisition error occurs before the
71
+ write and is not wrapped. Never treat an unknown outcome as a clean failure.
72
+
73
+ Intuit documents request-ID replay in its [API best practices](https://blogs.a.intuit.com/2018/09/10/quickbooks-online-api-best-practices/)
74
+ and [request ID field contract](https://developer.intuit.com/app/developer/qbo/docs/learn/learn-basic-field-definitions#request-id).
75
+ The [static documentation](https://static.developer.intuit.com/output_html/qbo/docs/learn/learn-basic-field-definitions.html)
76
+ confirms the 50-character limit and per-realm uniqueness requirement.
77
+ Our regression tests mock remote acceptance and response loss; they prove SDK
78
+ identity/byte preservation and retry policy, **not live Intuit deduplication**.
79
+ No sandbox credentials were available for this change. Before relying on remote
80
+ idempotency, verify in an authorized sandbox: send a uniquely identified invoice,
81
+ repeat the identical request with the same ID, confirm the same Invoice.Id and
82
+ one invoice, and simulate a lost response before replaying it. Record realm,
83
+ environment, request ID, timestamps, and returned IDs without credentials.
84
+ No replay retention period or exactly-once guarantee is asserted here; reconcile
85
+ old or uncertain requests against QuickBooks before any new creation identity.
86
+
5
87
  ## Stripe invoices
6
88
 
7
89
  Accounting amounts use currency major units: `12.34` means USD 12.34 and
@@ -1,4 +1,4 @@
1
- import { a as __toCommonJS, i as __require, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./quickbooks-DujZt_Gx.js";
1
+ import { a as __toCommonJS, i as __require, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./quickbooks-DLkypnT9.js";
2
2
  //#region ../../node_modules/.pnpm/atob@2.1.2/node_modules/atob/browser-atob.js
3
3
  var require_browser_atob = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4
4
  (function(w) {
@@ -18261,4 +18261,4 @@ var require_OAuthClient = /* @__PURE__ */ __commonJSMin(((exports, module) => {
18261
18261
  //#endregion
18262
18262
  export default require_OAuthClient();
18263
18263
 
18264
- //# sourceMappingURL=OAuthClient-DgRaQp_Y.js.map
18264
+ //# sourceMappingURL=OAuthClient-CF65pAW_.js.map