@happyvertical/accounting 0.91.0 → 0.94.0

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
@@ -37,11 +37,170 @@ complete billing address before creating the invoice so Stripe can calculate
37
37
  tax from that customer tax location. Read the invoice back to persist Stripe's
38
38
  calculated `taxAmount`; callers do not supply local tax tables or rates.
39
39
 
40
+ Invoice lines carry more than an amount:
41
+
42
+ - `periodStart` / `periodEnd` (set both) become the Stripe invoice item's
43
+ service `period`, so the invoice PDF, portal, and revenue reports show the
44
+ billed window. `periodEnd` is exclusive, like Stripe's own subscription
45
+ periods, and must not be before `periodStart`.
46
+ - `discount` is the line's total discount in major units. Stripe receives it
47
+ as an amount-off coupon on the invoice item (one reusable coupon per
48
+ currency and amount, id `hv_amount_off_<currency>_<stripe amount>`), so the
49
+ discount shows on the invoice and Stripe Tax taxes the discounted amount.
50
+
51
+ `collectionMethod: 'charge_automatically'` bills the customer's default
52
+ payment method instead of emailing a payable invoice (the default,
53
+ `send_invoice`). `invoices.send()` then finalizes the invoice with automatic
54
+ collection on: Stripe charges the card on its own collection schedule (not
55
+ necessarily at once, so activate service on `invoice.paid` rather than on
56
+ `send()`), retries failures under your Stripe retry settings, and reports the
57
+ result as `invoice.paid` or `invoice.payment_failed`. Automatically charged invoices carry no due date.
58
+ `invoices.markUncollectible()` writes an open invoice off, and invoice reads
59
+ report that state as `status: 'uncollectible'`.
60
+
61
+ `invoices.markPaidOutOfBand()` closes an invoice that was paid on another
62
+ rail (for example a crypto payment for a Stripe-issued invoice): Stripe marks
63
+ it `paid_out_of_band`, which stops its emails, dunning, and automatic charges.
64
+ It is idempotent (an invoice already closed out of band, or paid with
65
+ nothing collected — a zero total or a credit balance — is left alone), throws
66
+ for an invoice Stripe collected money for itself (a second collection needs a
67
+ refund, not a silent close), finalizes a draft first without automatic
68
+ collection and refuses it if finalization raised the amount due, pays an
69
+ uncollectible invoice, and refuses a void one. Invoice reads report `paidOutOfBand`, so a consumer can tell such
70
+ an invoice from one Stripe collected.
71
+
72
+ ## Stripe customers
73
+
74
+ Pass a stable `idempotencyKey` when a retry might create the customer again
75
+ (for example a worker that crashed before storing the returned id). A retry
76
+ with the same key returns the first customer: Stripe replays the create
77
+ inside its idempotency window, and after it the adapter finds the customer by
78
+ the `local_id` metadata it writes on every customer (the oldest one wins if
79
+ earlier retries left duplicates). Reuse the key only for the same customer
80
+ contents.
81
+
82
+ ```ts
83
+ const { externalId } = await stripe.customers.sync({
84
+ id: account.id,
85
+ name: account.name,
86
+ billingAddress: account.address,
87
+ idempotencyKey: `billing-account:${account.id}`,
88
+ });
89
+ ```
90
+
40
91
  ## Stripe Checkout and webhooks
41
92
 
42
93
  `billing.createCheckoutSession` accepts `idempotencyKey`; reuse it when
43
- retrying the same Checkout Session creation. After `webhooks.verify` succeeds,
44
- the parsed Stripe webhook exposes its provider event as `WebhookEvent.id`.
94
+ retrying the same Checkout Session creation.
95
+
96
+ Price ad-hoc Checkout lines with `priceData.unitAmountMinor`, integer ISO 4217
97
+ minor units that the adapter converts to Stripe's unit (ISK and UGX are
98
+ two-decimal at Stripe). `priceData.unitAmount` is still accepted and passed
99
+ through unconverted in Stripe's unit. Set `automaticTax: true` to charge tax
100
+ with Stripe Tax; Stripe needs the buyer's location, so collect it with
101
+ `billingAddressCollection` or, for an existing customer, save it with
102
+ `customerUpdate: { address: 'auto' }`. Ad-hoc prices default to
103
+ `taxBehavior: 'exclusive'` when tax is on.
104
+
105
+ ```ts
106
+ const session = await stripe.billing.createCheckoutSession({
107
+ mode: 'payment',
108
+ customerExternalId: 'cus_123',
109
+ lineItems: [
110
+ {
111
+ priceData: { currency: 'CAD', unitAmountMinor: 5000, productName: 'Credit' },
112
+ },
113
+ ],
114
+ automaticTax: true,
115
+ customerUpdate: { address: 'auto' },
116
+ setupFutureUsage: 'off_session', // also keep the card for top-ups
117
+ successUrl,
118
+ cancelUrl,
119
+ idempotencyKey: `credit-purchase:${cartId}`,
120
+ });
121
+ ```
122
+
123
+ ### Card on file
124
+
125
+ A `setup` mode session saves a payment method without charging. It takes no
126
+ line items and needs `currency` (or `paymentMethodTypes`). After it completes,
127
+ read the saved method and make it the customer's default so automatically
128
+ charged invoices and off-session charges can use it:
129
+
130
+ ```ts
131
+ const setup = await stripe.billing.createCheckoutSession({
132
+ mode: 'setup',
133
+ customerExternalId: 'cus_123',
134
+ currency: 'CAD',
135
+ billingAddressCollection: 'required',
136
+ customerUpdate: { address: 'auto', name: 'auto' },
137
+ successUrl,
138
+ cancelUrl,
139
+ idempotencyKey: `card-setup:${accountId}:${attempt}`,
140
+ });
141
+ // On checkout.session.completed:
142
+ // Both methods are optional on StripeBillingOperations; StripeProvider has them.
143
+ const done = await stripe.billing.retrieveCheckoutSession?.(setup.externalId);
144
+ if (done?.status === 'complete' && done.paymentMethodExternalId) {
145
+ await stripe.billing.setDefaultPaymentMethod?.(
146
+ 'cus_123',
147
+ done.paymentMethodExternalId,
148
+ );
149
+ }
150
+ ```
151
+
152
+ ## Off-session charges
153
+
154
+ `payments.chargeSavedPaymentMethod()` charges a saved payment method without
155
+ the customer present, for example an automatic prepaid-credit top-up. It is
156
+ provider-neutral and optional on `PaymentOperations`: a provider that cannot
157
+ pull funds from a stored method (such as a push-payment BTC rail) does not
158
+ implement it, so check for it before calling. Amounts are integer minor units
159
+ (`amountMinor`), and `idempotencyKey` is required.
160
+
161
+ ```ts
162
+ const charge = await stripe.payments.chargeSavedPaymentMethod?.({
163
+ customerExternalId: 'cus_123', // default payment method unless one is given
164
+ amountMinor: 2500,
165
+ currency: 'USD',
166
+ idempotencyKey: `topup:${policyId}:${sequence}`,
167
+ metadata: { policy: policyId },
168
+ });
169
+ switch (charge?.status) {
170
+ case 'succeeded': // funds secured: grant the credit
171
+ case 'processing': // wait for the payment webhook
172
+ case 'requires_action': // issuer wants authentication; re-save the card on-session
173
+ case 'failed': // declined or no payment method; see failureCode
174
+ }
175
+ ```
176
+
177
+ A decline (`failureCode: 'card_declined'`) or an authentication requirement
178
+ (`requires_action`, `failureCode: 'authentication_required'`) is returned, not
179
+ thrown. Results never include Stripe's client secret, so a `requires_action`
180
+ attempt is not completed later: when the customer is next present, collect
181
+ authentication with a `setup` mode Checkout session and
182
+ `setDefaultPaymentMethod`, then charge again with a new idempotency key (the
183
+ old key returns the original, unauthenticated attempt). Other Stripe API
184
+ errors throw a `StripeApiError` carrying `status`, `type`, and `code`;
185
+ invalid input and a reused-key refusal throw a plain `Error` before any
186
+ provider request. Every retry with the same key returns the original
187
+ charge: Stripe replays it inside its idempotency window, and after it the
188
+ adapter finds the PaymentIntent by its `hv_charge_key` metadata. Scope keys
189
+ to the payer and use a new key for a new attempt: a key reused for a
190
+ different amount, currency, customer, or explicit payment method is refused
191
+ (Stripe `idempotency_error`, or an adapter error after the window), never
192
+ charged. Pass `paymentMethodExternalId` to keep retries identical when the
193
+ customer's default card may change in between. Payment webhooks (`payment_intent.*`) carry a normalized `WebhookEvent.payment`
194
+ summary with the status, minor-unit amount, and `chargeKey`.
195
+
196
+ Minor-unit amounts (`amountMinor`, `unitAmountMinor`, and the `*Minor` read
197
+ fields) use ISO 4217 exponents from an explicit table, not the runtime's
198
+ `Intl` display digits, which differ for some currencies (for example RSD,
199
+ IQD, and MGA). Derive minor units the same way before calling.
200
+
201
+ ## Stripe webhooks
202
+
203
+ After `webhooks.verify` succeeds, the parsed Stripe webhook exposes its provider event as `WebhookEvent.id`.
45
204
  Persist that identifier in a durable inbox before applying side effects; the
46
205
  in-process parser alone cannot deduplicate deliveries across restarts or
47
206
  replicas.
@@ -1,4 +1,4 @@
1
- import { a as __toCommonJS, i as __require, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./quickbooks-BF3GrJBw.js";
1
+ import { a as __toCommonJS, i as __require, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./quickbooks-DujZt_Gx.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-Bv-lzpx8.js.map
18264
+ //# sourceMappingURL=OAuthClient-DgRaQp_Y.js.map