@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 +161 -2
- package/dist/chunks/{OAuthClient-Bv-lzpx8.js → OAuthClient-DgRaQp_Y.js} +2 -2
- package/dist/chunks/{OAuthClient-Bv-lzpx8.js.map → OAuthClient-DgRaQp_Y.js.map} +1 -1
- package/dist/chunks/errors-Dvn2QzOB.js +49 -0
- package/dist/chunks/errors-Dvn2QzOB.js.map +1 -0
- package/dist/chunks/{quickbooks-BF3GrJBw.js → quickbooks-DujZt_Gx.js} +3 -2
- package/dist/chunks/{quickbooks-BF3GrJBw.js.map → quickbooks-DujZt_Gx.js.map} +1 -1
- package/dist/chunks/{stripe-e3yn8cEJ.js → stripe-Dc061lPD.js} +478 -34
- package/dist/chunks/stripe-Dc061lPD.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/providers/stripe/index.d.ts +10 -1
- package/dist/providers/stripe/index.d.ts.map +1 -1
- package/dist/types.d.ts +270 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/stripe-e3yn8cEJ.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as StripeApiError } from "./errors-Dvn2QzOB.js";
|
|
1
2
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
3
|
//#region src/providers/stripe/index.ts
|
|
3
4
|
/**
|
|
@@ -14,6 +15,7 @@ var StripeProvider = class {
|
|
|
14
15
|
type = "stripe";
|
|
15
16
|
options;
|
|
16
17
|
fetchImpl;
|
|
18
|
+
couponIds = /* @__PURE__ */ new Map();
|
|
17
19
|
customers;
|
|
18
20
|
invoices;
|
|
19
21
|
vendors;
|
|
@@ -42,7 +44,7 @@ var StripeProvider = class {
|
|
|
42
44
|
}
|
|
43
45
|
async request(method, endpoint, params, options = {}) {
|
|
44
46
|
const timeout = this.options.timeout || 3e4;
|
|
45
|
-
const maxRetries = this.options.maxRetries
|
|
47
|
+
const maxRetries = this.options.maxRetries ?? 3;
|
|
46
48
|
const idempotencyKey = method === "POST" ? options.idempotencyKey ?? `sdk-${randomUUID()}` : null;
|
|
47
49
|
let lastError = null;
|
|
48
50
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -68,7 +70,7 @@ var StripeProvider = class {
|
|
|
68
70
|
});
|
|
69
71
|
if (!response.ok) {
|
|
70
72
|
const errorText = await response.text();
|
|
71
|
-
const error =
|
|
73
|
+
const error = new StripeApiError(response.status, errorText);
|
|
72
74
|
if (response.status < 500 && response.status !== 429) throw error;
|
|
73
75
|
lastError = error;
|
|
74
76
|
} else {
|
|
@@ -89,6 +91,42 @@ var StripeProvider = class {
|
|
|
89
91
|
}
|
|
90
92
|
throw lastError || /* @__PURE__ */ new Error("Stripe request failed after retries");
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
* A reusable amount-off coupon for a line discount, created on first use.
|
|
97
|
+
* Its id is derived from the currency and amount, so every replay and every
|
|
98
|
+
* invoice with the same discount shares one coupon instead of creating one
|
|
99
|
+
* per line.
|
|
100
|
+
*/
|
|
101
|
+
async amountOffCoupon(currency, amount) {
|
|
102
|
+
const normalized = currency.toLowerCase();
|
|
103
|
+
const id = `hv_amount_off_${normalized}_${amount}`;
|
|
104
|
+
const cached = this.couponIds.get(id);
|
|
105
|
+
if (cached) return cached;
|
|
106
|
+
const path = `/v1/coupons/${encodeURIComponent(id)}`;
|
|
107
|
+
let coupon;
|
|
108
|
+
try {
|
|
109
|
+
coupon = await this.request("GET", path);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (!(error instanceof StripeApiError) || error.status !== 404) throw error;
|
|
112
|
+
try {
|
|
113
|
+
coupon = await this.request("POST", "/v1/coupons", {
|
|
114
|
+
id,
|
|
115
|
+
amount_off: amount,
|
|
116
|
+
currency: normalized,
|
|
117
|
+
duration: "forever",
|
|
118
|
+
name: "Discount",
|
|
119
|
+
metadata: { hv_purpose: "invoice_line_discount" }
|
|
120
|
+
});
|
|
121
|
+
} catch (createError) {
|
|
122
|
+
if (!(createError instanceof StripeApiError) || createError.code !== "resource_already_exists") throw createError;
|
|
123
|
+
coupon = await this.request("GET", path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (coupon.amount_off !== amount || (coupon.currency || "").toLowerCase() !== normalized || coupon.percent_off || coupon.valid === false) throw new Error(`Stripe coupon ${id} exists but is not a valid ${amount} ${normalized} amount-off coupon`);
|
|
127
|
+
this.couponIds.set(id, coupon.id);
|
|
128
|
+
return coupon.id;
|
|
129
|
+
}
|
|
92
130
|
buildUrl(endpoint, params) {
|
|
93
131
|
const baseUrl = this.options.apiBaseUrl || "https://api.stripe.com";
|
|
94
132
|
const url = new URL(endpoint, baseUrl);
|
|
@@ -102,12 +140,40 @@ var StripeCustomerOperations = class {
|
|
|
102
140
|
this.provider = provider;
|
|
103
141
|
}
|
|
104
142
|
async push(customer) {
|
|
143
|
+
if (customer.idempotencyKey !== void 0) {
|
|
144
|
+
assertIdempotencyKey(customer.idempotencyKey);
|
|
145
|
+
const existing = await this.findByLocalId(customer.id);
|
|
146
|
+
if (existing) return {
|
|
147
|
+
action: "created",
|
|
148
|
+
externalId: existing.id,
|
|
149
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
150
|
+
};
|
|
151
|
+
}
|
|
105
152
|
return {
|
|
106
153
|
action: "created",
|
|
107
|
-
externalId: (await this.provider.request("POST", "/v1/customers", mapCustomerToStripe(customer))).id,
|
|
154
|
+
externalId: (await this.provider.request("POST", "/v1/customers", mapCustomerToStripe(customer), { idempotencyKey: customer.idempotencyKey ? `${customer.idempotencyKey}:customer` : void 0 })).id,
|
|
108
155
|
syncedAt: /* @__PURE__ */ new Date()
|
|
109
156
|
};
|
|
110
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The oldest customer tagged with this local id. Stripe search is
|
|
160
|
+
* eventually consistent (normally under a minute); inside that lag the
|
|
161
|
+
* idempotency key, retained for at least 24 hours, covers the replay.
|
|
162
|
+
*/
|
|
163
|
+
async findByLocalId(localId) {
|
|
164
|
+
const matches = [];
|
|
165
|
+
let page;
|
|
166
|
+
do {
|
|
167
|
+
const response = await this.provider.request("GET", "/v1/customers/search", {
|
|
168
|
+
query: `metadata['local_id']:'${escapeSearchValue(localId)}'`,
|
|
169
|
+
limit: 100,
|
|
170
|
+
page
|
|
171
|
+
});
|
|
172
|
+
matches.push(...response.data.filter((customer) => customer.metadata?.local_id === localId));
|
|
173
|
+
page = response.has_more ? response.next_page || void 0 : void 0;
|
|
174
|
+
} while (page);
|
|
175
|
+
return matches.sort((left, right) => (left.created ?? 0) - (right.created ?? 0) || left.id.localeCompare(right.id))[0];
|
|
176
|
+
}
|
|
111
177
|
async pull(externalId) {
|
|
112
178
|
return mapStripeCustomer(await this.provider.request("GET", `/v1/customers/${encodeURIComponent(externalId)}`));
|
|
113
179
|
}
|
|
@@ -131,6 +197,14 @@ var StripeInvoiceOperations = class {
|
|
|
131
197
|
}
|
|
132
198
|
async push(invoice) {
|
|
133
199
|
if (!invoice.customerExternalId) throw new Error("Stripe invoices require customerExternalId");
|
|
200
|
+
const currency = invoice.currency || "usd";
|
|
201
|
+
const collectionMethod = resolveCollectionMethod(invoice);
|
|
202
|
+
const lines = invoice.lineItems.map((lineItem) => ({
|
|
203
|
+
lineItem,
|
|
204
|
+
unitAmount: moneyToStripeMinorUnits(lineItem.unitPrice, currency),
|
|
205
|
+
discount: lineDiscountToStripe(lineItem, currency),
|
|
206
|
+
period: lineServicePeriod(lineItem)
|
|
207
|
+
}));
|
|
134
208
|
const existingInvoice = invoice.idempotencyKey ? await this.findInvoiceByLocalId(invoice.customerExternalId, invoice.id) : void 0;
|
|
135
209
|
if (existingInvoice) return {
|
|
136
210
|
action: "created",
|
|
@@ -138,14 +212,18 @@ var StripeInvoiceOperations = class {
|
|
|
138
212
|
syncedAt: /* @__PURE__ */ new Date()
|
|
139
213
|
};
|
|
140
214
|
const existingLineItemIndexes = invoice.idempotencyKey ? await this.findPendingLineItemIndexes(invoice.customerExternalId, invoice.id) : /* @__PURE__ */ new Set();
|
|
141
|
-
for (const [index,
|
|
215
|
+
for (const [index, line] of lines.entries()) {
|
|
142
216
|
if (existingLineItemIndexes.has(index)) continue;
|
|
217
|
+
const { lineItem, unitAmount, discount, period } = line;
|
|
218
|
+
const coupon = discount ? await this.provider.amountOffCoupon(currency, discount) : void 0;
|
|
143
219
|
await this.provider.request("POST", "/v1/invoiceitems", {
|
|
144
220
|
customer: invoice.customerExternalId,
|
|
145
|
-
currency
|
|
221
|
+
currency,
|
|
146
222
|
description: lineItem.description,
|
|
147
223
|
quantity: lineItem.quantity,
|
|
148
|
-
unit_amount_decimal: String(
|
|
224
|
+
unit_amount_decimal: String(unitAmount),
|
|
225
|
+
period,
|
|
226
|
+
discounts: coupon ? [{ coupon }] : void 0,
|
|
149
227
|
tax_behavior: invoice.automaticTax ? "exclusive" : void 0,
|
|
150
228
|
metadata: normalizeMetadata({
|
|
151
229
|
local_invoice_id: invoice.id,
|
|
@@ -158,7 +236,7 @@ var StripeInvoiceOperations = class {
|
|
|
158
236
|
}
|
|
159
237
|
return {
|
|
160
238
|
action: "created",
|
|
161
|
-
externalId: (await this.provider.request("POST", "/v1/invoices", mapInvoiceToStripe(invoice), { idempotencyKey: invoice.idempotencyKey ? `${invoice.idempotencyKey}:invoice` : void 0 })).id,
|
|
239
|
+
externalId: (await this.provider.request("POST", "/v1/invoices", mapInvoiceToStripe(invoice, collectionMethod), { idempotencyKey: invoice.idempotencyKey ? `${invoice.idempotencyKey}:invoice` : void 0 })).id,
|
|
162
240
|
syncedAt: /* @__PURE__ */ new Date()
|
|
163
241
|
};
|
|
164
242
|
}
|
|
@@ -170,15 +248,59 @@ var StripeInvoiceOperations = class {
|
|
|
170
248
|
}
|
|
171
249
|
async sync(invoice) {
|
|
172
250
|
if (!invoice.externalId) return this.push(invoice);
|
|
173
|
-
await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(invoice.externalId)}`, mapInvoiceUpdateToStripe(invoice));
|
|
251
|
+
await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(invoice.externalId)}`, mapInvoiceUpdateToStripe(invoice, resolveCollectionMethod(invoice)));
|
|
174
252
|
return {
|
|
175
253
|
action: "updated",
|
|
176
254
|
externalId: invoice.externalId,
|
|
177
255
|
syncedAt: /* @__PURE__ */ new Date()
|
|
178
256
|
};
|
|
179
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* `send_invoice` invoices are emailed through Stripe's send endpoint.
|
|
260
|
+
* `charge_automatically` invoices are finalized with automatic collection
|
|
261
|
+
* on, so Stripe charges the customer's default payment method and applies
|
|
262
|
+
* its retry settings; the outcome arrives as `invoice.paid` or
|
|
263
|
+
* `invoice.payment_failed`.
|
|
264
|
+
*/
|
|
180
265
|
async send(externalId) {
|
|
181
|
-
|
|
266
|
+
const path = `/v1/invoices/${encodeURIComponent(externalId)}`;
|
|
267
|
+
const invoice = await this.provider.request("GET", path);
|
|
268
|
+
if (invoice.collection_method !== "charge_automatically") {
|
|
269
|
+
await this.provider.request("POST", `${path}/send`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (invoice.status === "draft") {
|
|
273
|
+
await this.provider.request("POST", `${path}/finalize`, { auto_advance: true }, { idempotencyKey: `${externalId}:finalize` });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (invoice.status === "open" && invoice.auto_advance === false) await this.provider.request("POST", path, { auto_advance: true }, { idempotencyKey: `${externalId}:auto_advance` });
|
|
277
|
+
}
|
|
278
|
+
async markUncollectible(externalId) {
|
|
279
|
+
await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(externalId)}/mark_uncollectible`, void 0, { idempotencyKey: `${externalId}:mark_uncollectible` });
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Close an invoice paid on another rail (`paid_out_of_band`). An invoice
|
|
283
|
+
* already marked paid out of band, or paid with nothing collected, is left
|
|
284
|
+
* as is; one Stripe collected money for itself throws (a second collection needs a refund, not a silent close).
|
|
285
|
+
* A draft is finalized first without automatic collection, and refused if
|
|
286
|
+
* finalization raised the amount due. A void invoice throws; an
|
|
287
|
+
* uncollectible one can still be paid, as in Stripe.
|
|
288
|
+
*/
|
|
289
|
+
async markPaidOutOfBand(externalId) {
|
|
290
|
+
const path = `/v1/invoices/${encodeURIComponent(externalId)}`;
|
|
291
|
+
let invoice = await this.provider.request("GET", path);
|
|
292
|
+
if (invoice.status === "paid") {
|
|
293
|
+
if (invoice.paid_out_of_band === true || !(invoice.amount_paid ?? 0)) return;
|
|
294
|
+
throw new Error(`Stripe invoice ${externalId} was already paid through Stripe; it was not marked paid out of band.`);
|
|
295
|
+
}
|
|
296
|
+
if (invoice.status === "void") throw new Error(`Stripe invoice ${externalId} is void and cannot be marked paid.`);
|
|
297
|
+
if (invoice.status === "draft") {
|
|
298
|
+
const draftDue = invoice.amount_remaining ?? invoice.total ?? 0;
|
|
299
|
+
invoice = await this.provider.request("POST", `${path}/finalize`, { auto_advance: false }, { idempotencyKey: `${externalId}:finalize_out_of_band` });
|
|
300
|
+
if (invoice.status === "paid") return;
|
|
301
|
+
if ((invoice.amount_remaining ?? 0) > draftDue) throw new Error(`Stripe invoice ${externalId} grew from ${draftDue} to ${invoice.amount_remaining} on finalization; not marking it paid out of band.`);
|
|
302
|
+
}
|
|
303
|
+
await this.provider.request("POST", `${path}/pay`, { paid_out_of_band: true }, { idempotencyKey: `${externalId}:paid_out_of_band` });
|
|
182
304
|
}
|
|
183
305
|
async void(externalId) {
|
|
184
306
|
await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(externalId)}/void`);
|
|
@@ -227,6 +349,79 @@ var StripePaymentOperations = class {
|
|
|
227
349
|
async list(options = {}) {
|
|
228
350
|
return (await this.provider.request("GET", "/v1/payment_intents", mapListOptions(options))).data.map(mapStripePaymentIntent);
|
|
229
351
|
}
|
|
352
|
+
async chargeSavedPaymentMethod(input) {
|
|
353
|
+
assertIdempotencyKey(input.idempotencyKey);
|
|
354
|
+
if (!input.customerExternalId) throw new Error("Saved payment method charges require a customer");
|
|
355
|
+
const currency = normalizeCurrencyCode(input.currency);
|
|
356
|
+
const stripeAmount = minorUnitsToStripeAmount(input.amountMinor, currency);
|
|
357
|
+
if (stripeAmount <= 0) throw new Error("Charge amount must be a positive integer of minor units");
|
|
358
|
+
const chargeKey = input.idempotencyKey;
|
|
359
|
+
const base = {
|
|
360
|
+
provider: "stripe",
|
|
361
|
+
customerExternalId: input.customerExternalId,
|
|
362
|
+
amountMinor: input.amountMinor,
|
|
363
|
+
currency,
|
|
364
|
+
chargeKey
|
|
365
|
+
};
|
|
366
|
+
const existing = await this.findByChargeKey(chargeKey);
|
|
367
|
+
if (existing) {
|
|
368
|
+
assertSameCharge(existing, input.customerExternalId, stripeAmount, currency, input.paymentMethodExternalId);
|
|
369
|
+
return {
|
|
370
|
+
...base,
|
|
371
|
+
...mapChargeOutcome(existing)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
let paymentMethod = input.paymentMethodExternalId;
|
|
375
|
+
if (!paymentMethod) {
|
|
376
|
+
paymentMethod = idOf((await this.provider.request("GET", `/v1/customers/${encodeURIComponent(input.customerExternalId)}`)).invoice_settings?.default_payment_method);
|
|
377
|
+
if (!paymentMethod) return {
|
|
378
|
+
...base,
|
|
379
|
+
status: "failed",
|
|
380
|
+
failureCode: "payment_method_missing",
|
|
381
|
+
failureMessage: "The customer has no default payment method."
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const paymentIntent = await this.provider.request("POST", "/v1/payment_intents", {
|
|
386
|
+
amount: stripeAmount,
|
|
387
|
+
currency: currency.toLowerCase(),
|
|
388
|
+
customer: input.customerExternalId,
|
|
389
|
+
payment_method: paymentMethod,
|
|
390
|
+
off_session: true,
|
|
391
|
+
confirm: true,
|
|
392
|
+
description: input.description,
|
|
393
|
+
metadata: normalizeMetadata({
|
|
394
|
+
...input.metadata,
|
|
395
|
+
[CHARGE_KEY_METADATA]: chargeKey
|
|
396
|
+
})
|
|
397
|
+
}, { idempotencyKey: `${chargeKey}:payment_intent` });
|
|
398
|
+
return {
|
|
399
|
+
...base,
|
|
400
|
+
...mapChargeOutcome(paymentIntent)
|
|
401
|
+
};
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof StripeApiError && error.status === 402 && error.type === "card_error") {
|
|
404
|
+
const paymentIntent = isRecord(error.paymentIntent) ? error.paymentIntent : void 0;
|
|
405
|
+
return {
|
|
406
|
+
...base,
|
|
407
|
+
...paymentIntent ? mapChargeOutcome(paymentIntent) : {},
|
|
408
|
+
paymentMethodExternalId: idOf(paymentIntent?.payment_method) ?? paymentMethod,
|
|
409
|
+
status: error.code === "authentication_required" ? "requires_action" : "failed",
|
|
410
|
+
failureCode: error.code,
|
|
411
|
+
declineCode: error.declineCode,
|
|
412
|
+
failureMessage: error.stripeMessage,
|
|
413
|
+
raw: paymentIntent ? withoutClientSecret(paymentIntent) : void 0
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async findByChargeKey(chargeKey) {
|
|
420
|
+
return (await this.provider.request("GET", "/v1/payment_intents/search", {
|
|
421
|
+
query: `metadata['${CHARGE_KEY_METADATA}']:'${escapeSearchValue(chargeKey)}'`,
|
|
422
|
+
limit: 10
|
|
423
|
+
})).data.filter((intent) => intent.metadata?.[CHARGE_KEY_METADATA] === chargeKey).sort((left, right) => (left.created ?? 0) - (right.created ?? 0) || left.id.localeCompare(right.id))[0];
|
|
424
|
+
}
|
|
230
425
|
};
|
|
231
426
|
var StripeBillingOperationsImpl = class {
|
|
232
427
|
provider;
|
|
@@ -234,16 +429,38 @@ var StripeBillingOperationsImpl = class {
|
|
|
234
429
|
this.provider = provider;
|
|
235
430
|
}
|
|
236
431
|
async createCheckoutSession(input) {
|
|
237
|
-
|
|
432
|
+
return mapCheckoutSessionResponse(await this.provider.request("POST", "/v1/checkout/sessions", mapCheckoutSessionToStripe(input), { idempotencyKey: input.idempotencyKey }));
|
|
433
|
+
}
|
|
434
|
+
async retrieveCheckoutSession(sessionExternalId) {
|
|
435
|
+
const session = await this.provider.request("GET", `/v1/checkout/sessions/${encodeURIComponent(sessionExternalId)}`, { expand: ["setup_intent", "payment_intent"] });
|
|
436
|
+
const intent = typeof session.setup_intent === "object" && session.setup_intent ? session.setup_intent : typeof session.payment_intent === "object" && session.payment_intent ? session.payment_intent : void 0;
|
|
437
|
+
const currency = session.currency ? session.currency.toUpperCase() : void 0;
|
|
438
|
+
const minor = (amount) => currency && typeof amount === "number" ? stripeAmountToMinorUnitsOrUndefined(amount, currency) : void 0;
|
|
238
439
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
440
|
+
...mapCheckoutSessionResponse(session),
|
|
441
|
+
raw: withoutClientSecrets(session),
|
|
442
|
+
status: oneOf(session.status, [
|
|
443
|
+
"open",
|
|
444
|
+
"complete",
|
|
445
|
+
"expired"
|
|
446
|
+
]),
|
|
447
|
+
paymentStatus: oneOf(session.payment_status, [
|
|
448
|
+
"paid",
|
|
449
|
+
"unpaid",
|
|
450
|
+
"no_payment_required"
|
|
451
|
+
]),
|
|
452
|
+
paymentMethodExternalId: idOf(intent?.payment_method),
|
|
453
|
+
currency,
|
|
454
|
+
amountSubtotalMinor: minor(session.amount_subtotal),
|
|
455
|
+
amountTaxMinor: minor(session.total_details?.amount_tax),
|
|
456
|
+
amountTotalMinor: minor(session.amount_total),
|
|
457
|
+
metadata: { ...session.metadata || {} }
|
|
245
458
|
};
|
|
246
459
|
}
|
|
460
|
+
async setDefaultPaymentMethod(customerExternalId, paymentMethodExternalId) {
|
|
461
|
+
if (!customerExternalId || !paymentMethodExternalId) throw new Error("setDefaultPaymentMethod requires a customer and a payment method");
|
|
462
|
+
await this.provider.request("POST", `/v1/customers/${encodeURIComponent(customerExternalId)}`, { invoice_settings: { default_payment_method: paymentMethodExternalId } });
|
|
463
|
+
}
|
|
247
464
|
async createCustomerPortalSession(input) {
|
|
248
465
|
const response = await this.provider.request("POST", "/v1/billing_portal/sessions", {
|
|
249
466
|
customer: input.customerExternalId,
|
|
@@ -350,7 +567,9 @@ var StripeWebhookOperations = class {
|
|
|
350
567
|
throw new Error(`Invalid Stripe webhook payload: ${message}`);
|
|
351
568
|
}
|
|
352
569
|
const resource = event?.data?.object;
|
|
570
|
+
const payment = resource?.object === "payment_intent" && typeof resource.id === "string" ? mapWebhookPayment(resource, event?.type || void 0) : void 0;
|
|
353
571
|
return {
|
|
572
|
+
...payment ? { payment } : {},
|
|
354
573
|
type: event?.type || "unknown",
|
|
355
574
|
id: typeof event?.id === "string" ? event.id : void 0,
|
|
356
575
|
provider: "stripe",
|
|
@@ -398,19 +617,19 @@ function mapCustomerToStripe(customer) {
|
|
|
398
617
|
country: customer.billingAddress.country
|
|
399
618
|
} : void 0,
|
|
400
619
|
metadata: normalizeMetadata({
|
|
401
|
-
local_id: customer.id,
|
|
402
620
|
payment_terms: customer.paymentTerms,
|
|
403
|
-
...customer.metadata
|
|
621
|
+
...customer.metadata,
|
|
622
|
+
local_id: customer.id
|
|
404
623
|
})
|
|
405
624
|
};
|
|
406
625
|
if (customer.taxExempt !== void 0) stripeCustomer.tax_exempt = customer.taxExempt ? "exempt" : "none";
|
|
407
626
|
return stripeCustomer;
|
|
408
627
|
}
|
|
409
|
-
function mapInvoiceToStripe(invoice) {
|
|
628
|
+
function mapInvoiceToStripe(invoice, collectionMethod) {
|
|
410
629
|
return {
|
|
411
630
|
customer: invoice.customerExternalId,
|
|
412
|
-
collection_method:
|
|
413
|
-
due_date: invoice.dueDate,
|
|
631
|
+
collection_method: collectionMethod,
|
|
632
|
+
due_date: collectionMethod === "send_invoice" ? invoice.dueDate : void 0,
|
|
414
633
|
auto_advance: false,
|
|
415
634
|
pending_invoice_items_behavior: "include",
|
|
416
635
|
automatic_tax: invoice.automaticTax ? { enabled: true } : void 0,
|
|
@@ -426,9 +645,10 @@ function mapInvoiceToStripe(invoice) {
|
|
|
426
645
|
})
|
|
427
646
|
};
|
|
428
647
|
}
|
|
429
|
-
function mapInvoiceUpdateToStripe(invoice) {
|
|
648
|
+
function mapInvoiceUpdateToStripe(invoice, collectionMethod) {
|
|
430
649
|
return {
|
|
431
|
-
|
|
650
|
+
collection_method: invoice.collectionMethod ? collectionMethod : void 0,
|
|
651
|
+
due_date: collectionMethod === "send_invoice" ? invoice.dueDate : void 0,
|
|
432
652
|
metadata: normalizeMetadata({
|
|
433
653
|
local_id: invoice.id,
|
|
434
654
|
invoice_number: invoice.invoiceNumber,
|
|
@@ -439,35 +659,76 @@ function mapInvoiceUpdateToStripe(invoice) {
|
|
|
439
659
|
};
|
|
440
660
|
}
|
|
441
661
|
function mapCheckoutSessionToStripe(input) {
|
|
662
|
+
const mode = input.mode || "subscription";
|
|
663
|
+
const lineItems = input.lineItems || [];
|
|
664
|
+
if (mode === "setup") {
|
|
665
|
+
if (lineItems.length > 0) throw new Error("Stripe setup-mode Checkout does not accept line items");
|
|
666
|
+
if (!input.currency && !input.paymentMethodTypes?.length) throw new Error("Stripe setup-mode Checkout requires currency or paymentMethodTypes");
|
|
667
|
+
if (input.automaticTax) throw new Error("Stripe setup-mode Checkout does not calculate tax");
|
|
668
|
+
} else if (lineItems.length === 0) throw new Error(`Stripe ${mode}-mode Checkout requires line items`);
|
|
669
|
+
if (input.setupFutureUsage && mode !== "payment") throw new Error("setupFutureUsage applies only to payment-mode Checkout");
|
|
670
|
+
if (input.customerUpdate && !input.customerExternalId) throw new Error("customerUpdate requires customerExternalId");
|
|
442
671
|
return {
|
|
443
|
-
mode
|
|
672
|
+
mode,
|
|
444
673
|
success_url: input.successUrl,
|
|
445
674
|
cancel_url: input.cancelUrl,
|
|
446
675
|
customer: input.customerExternalId,
|
|
447
676
|
customer_email: input.customerExternalId ? void 0 : input.customerEmail,
|
|
448
677
|
client_reference_id: input.clientReferenceId,
|
|
449
678
|
allow_promotion_codes: input.allowPromotionCodes,
|
|
450
|
-
|
|
679
|
+
currency: input.currency ? input.currency.toLowerCase() : void 0,
|
|
680
|
+
payment_method_types: input.paymentMethodTypes,
|
|
681
|
+
automatic_tax: input.automaticTax ? { enabled: true } : void 0,
|
|
682
|
+
billing_address_collection: input.billingAddressCollection,
|
|
683
|
+
customer_update: input.customerUpdate ? {
|
|
684
|
+
address: input.customerUpdate.address,
|
|
685
|
+
name: input.customerUpdate.name,
|
|
686
|
+
shipping: input.customerUpdate.shipping
|
|
687
|
+
} : void 0,
|
|
688
|
+
payment_intent_data: input.setupFutureUsage ? { setup_future_usage: input.setupFutureUsage } : void 0,
|
|
689
|
+
line_items: mode === "setup" ? void 0 : lineItems.map((lineItem) => mapCheckoutLineItem(lineItem, Boolean(input.automaticTax))),
|
|
451
690
|
metadata: normalizeMetadata(input.metadata || {})
|
|
452
691
|
};
|
|
453
692
|
}
|
|
454
|
-
function
|
|
693
|
+
function mapCheckoutSessionResponse(response) {
|
|
694
|
+
return {
|
|
695
|
+
externalId: response.id,
|
|
696
|
+
url: response.url || null,
|
|
697
|
+
mode: oneOf(response.mode, [
|
|
698
|
+
"payment",
|
|
699
|
+
"setup",
|
|
700
|
+
"subscription"
|
|
701
|
+
]),
|
|
702
|
+
customerExternalId: idOf(response.customer),
|
|
703
|
+
subscriptionExternalId: idOf(response.subscription),
|
|
704
|
+
paymentIntentExternalId: idOf(response.payment_intent),
|
|
705
|
+
setupIntentExternalId: idOf(response.setup_intent),
|
|
706
|
+
raw: withoutClientSecrets(response)
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function mapCheckoutLineItem(lineItem, automaticTax = false) {
|
|
455
710
|
if (lineItem.price) return {
|
|
456
711
|
price: lineItem.price,
|
|
457
712
|
quantity: lineItem.quantity || 1
|
|
458
713
|
};
|
|
459
714
|
if (!lineItem.priceData) throw new Error("Stripe checkout line item requires price or priceData");
|
|
460
|
-
const
|
|
715
|
+
const { priceData } = lineItem;
|
|
716
|
+
const productData = priceData.product ? void 0 : { name: priceData.productName || "Subscription" };
|
|
717
|
+
const hasStripeAmount = priceData.unitAmount !== void 0;
|
|
718
|
+
const hasMinorAmount = priceData.unitAmountMinor !== void 0;
|
|
719
|
+
if (hasStripeAmount === hasMinorAmount) throw new Error("Stripe checkout priceData requires exactly one of unitAmount or unitAmountMinor");
|
|
720
|
+
const unitAmount = hasMinorAmount ? minorUnitsToStripeAmount(priceData.unitAmountMinor, normalizeCurrencyCode(priceData.currency)) : priceData.unitAmount;
|
|
461
721
|
return {
|
|
462
722
|
quantity: lineItem.quantity || 1,
|
|
463
723
|
price_data: {
|
|
464
|
-
currency:
|
|
465
|
-
unit_amount:
|
|
466
|
-
|
|
724
|
+
currency: priceData.currency.toLowerCase(),
|
|
725
|
+
unit_amount: unitAmount,
|
|
726
|
+
tax_behavior: priceData.taxBehavior ?? (automaticTax ? "exclusive" : void 0),
|
|
727
|
+
product: priceData.product,
|
|
467
728
|
product_data: productData,
|
|
468
|
-
recurring:
|
|
469
|
-
interval:
|
|
470
|
-
interval_count:
|
|
729
|
+
recurring: priceData.recurring ? {
|
|
730
|
+
interval: priceData.recurring.interval,
|
|
731
|
+
interval_count: priceData.recurring.intervalCount
|
|
471
732
|
} : void 0
|
|
472
733
|
}
|
|
473
734
|
};
|
|
@@ -520,6 +781,7 @@ function mapStripeInvoice(invoice) {
|
|
|
520
781
|
balance: stripeMinorUnitsToMoney(invoice.amount_remaining || 0, invoice.currency),
|
|
521
782
|
status: mapStripeInvoiceStatus(invoice.status, invoice.due_date),
|
|
522
783
|
currency: invoice.currency || "usd",
|
|
784
|
+
paidOutOfBand: invoice.paid_out_of_band === true,
|
|
523
785
|
raw: invoice
|
|
524
786
|
};
|
|
525
787
|
}
|
|
@@ -556,6 +818,7 @@ function mapStripeInvoiceStatus(status, dueDate) {
|
|
|
556
818
|
case "draft": return "draft";
|
|
557
819
|
case "paid": return "paid";
|
|
558
820
|
case "void": return "voided";
|
|
821
|
+
case "uncollectible": return "uncollectible";
|
|
559
822
|
case "open":
|
|
560
823
|
if (dueDate && dueDate * 1e3 < Date.now()) return "overdue";
|
|
561
824
|
return "sent";
|
|
@@ -715,6 +978,187 @@ function moneyToStripeMinorUnits(amount, currency) {
|
|
|
715
978
|
function stripeMinorUnitsToMoney(amount, currency) {
|
|
716
979
|
return amount / stripeCurrencyMinorUnitFactor(currency);
|
|
717
980
|
}
|
|
981
|
+
var CHARGE_KEY_METADATA = "hv_charge_key";
|
|
982
|
+
function assertIdempotencyKey(key) {
|
|
983
|
+
if (typeof key !== "string" || key.trim() === "") throw new Error("idempotencyKey must be a non-empty string");
|
|
984
|
+
}
|
|
985
|
+
/** Escape a value for a single-quoted Stripe Search Query Language string. */
|
|
986
|
+
function escapeSearchValue(value) {
|
|
987
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
988
|
+
}
|
|
989
|
+
function resolveCollectionMethod(invoice) {
|
|
990
|
+
const method = invoice.collectionMethod ?? "send_invoice";
|
|
991
|
+
if (method !== "send_invoice" && method !== "charge_automatically") throw new Error(`Unsupported invoice collectionMethod '${method}'`);
|
|
992
|
+
return method;
|
|
993
|
+
}
|
|
994
|
+
/** The line's discount in Stripe units, validated against the line total. */
|
|
995
|
+
function lineDiscountToStripe(lineItem, currency) {
|
|
996
|
+
if (lineItem.discount === void 0 || lineItem.discount === 0) return;
|
|
997
|
+
if (!Number.isFinite(lineItem.discount) || lineItem.discount < 0) throw new Error(`Invoice line '${lineItem.description}' discount must be a non-negative amount`);
|
|
998
|
+
const discount = moneyToStripeMinorUnits(lineItem.discount, currency);
|
|
999
|
+
if (discount > Math.round(moneyToStripeMinorUnits(lineItem.unitPrice, currency) * lineItem.quantity)) throw new Error(`Invoice line '${lineItem.description}' discount exceeds the line total`);
|
|
1000
|
+
return discount > 0 ? discount : void 0;
|
|
1001
|
+
}
|
|
1002
|
+
function lineServicePeriod(lineItem) {
|
|
1003
|
+
const { periodStart, periodEnd } = lineItem;
|
|
1004
|
+
if (!periodStart && !periodEnd) return;
|
|
1005
|
+
if (!(periodStart instanceof Date) || !(periodEnd instanceof Date)) throw new Error(`Invoice line '${lineItem.description}' needs both periodStart and periodEnd`);
|
|
1006
|
+
if (Number.isNaN(periodStart.getTime()) || Number.isNaN(periodEnd.getTime()) || periodEnd.getTime() < periodStart.getTime()) throw new Error(`Invoice line '${lineItem.description}' has an invalid service period`);
|
|
1007
|
+
return {
|
|
1008
|
+
start: periodStart,
|
|
1009
|
+
end: periodEnd
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function mapChargeOutcome(paymentIntent) {
|
|
1013
|
+
const error = paymentIntent.last_payment_error;
|
|
1014
|
+
const status = mapPaymentIntentChargeStatus(paymentIntent.status, error?.code);
|
|
1015
|
+
return {
|
|
1016
|
+
status,
|
|
1017
|
+
paymentExternalId: paymentIntent.id,
|
|
1018
|
+
paymentMethodExternalId: idOf(paymentIntent.payment_method),
|
|
1019
|
+
failureCode: status === "succeeded" ? void 0 : error?.code || void 0,
|
|
1020
|
+
declineCode: status === "succeeded" ? void 0 : error?.decline_code || void 0,
|
|
1021
|
+
failureMessage: status === "succeeded" ? void 0 : error?.message || void 0,
|
|
1022
|
+
raw: withoutClientSecret(paymentIntent)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* A PaymentIntent found by charge key after Stripe's idempotency window must
|
|
1027
|
+
* be the charge this call describes; a key reused for a different amount,
|
|
1028
|
+
* currency, or customer is refused instead of reporting the wrong charge.
|
|
1029
|
+
*/
|
|
1030
|
+
function assertSameCharge(paymentIntent, customerExternalId, stripeAmount, currency, paymentMethodExternalId) {
|
|
1031
|
+
const customer = idOf(paymentIntent.customer);
|
|
1032
|
+
const paymentMethod = idOf(paymentIntent.payment_method);
|
|
1033
|
+
if (paymentIntent.amount !== stripeAmount || (paymentIntent.currency || "").toUpperCase() !== currency || customer !== void 0 && customer !== customerExternalId || paymentMethodExternalId !== void 0 && paymentMethod !== void 0 && paymentMethod !== paymentMethodExternalId) throw new Error(`idempotencyKey was already used for a different charge (${paymentIntent.id}); use a new key for a new charge`);
|
|
1034
|
+
}
|
|
1035
|
+
/** Remove `client_secret` from a Stripe object and its expanded intents. */
|
|
1036
|
+
function withoutClientSecrets(value) {
|
|
1037
|
+
const copy = withoutClientSecret(value);
|
|
1038
|
+
for (const key of ["payment_intent", "setup_intent"]) {
|
|
1039
|
+
const nested = copy[key];
|
|
1040
|
+
if (isRecord(nested)) copy[key] = withoutClientSecret(nested);
|
|
1041
|
+
}
|
|
1042
|
+
return copy;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Drop the intent client secret before handing objects to callers, including
|
|
1046
|
+
* `next_action`, whose redirect URL and SDK payload embed the same secret.
|
|
1047
|
+
*/
|
|
1048
|
+
function withoutClientSecret(value) {
|
|
1049
|
+
const { client_secret: _secret, next_action: _nextAction, ...rest } = value;
|
|
1050
|
+
return rest;
|
|
1051
|
+
}
|
|
1052
|
+
function mapPaymentIntentChargeStatus(status, errorCode) {
|
|
1053
|
+
switch (status) {
|
|
1054
|
+
case "succeeded": return "succeeded";
|
|
1055
|
+
case "processing":
|
|
1056
|
+
case "requires_capture": return "processing";
|
|
1057
|
+
case "requires_action":
|
|
1058
|
+
case "requires_confirmation": return "requires_action";
|
|
1059
|
+
case "canceled": return "canceled";
|
|
1060
|
+
case "requires_payment_method": return errorCode === "authentication_required" ? "requires_action" : "failed";
|
|
1061
|
+
default: return "processing";
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function mapWebhookPayment(paymentIntent, eventType) {
|
|
1065
|
+
const error = paymentIntent.last_payment_error;
|
|
1066
|
+
let status = mapPaymentIntentChargeStatus(paymentIntent.status, error?.code);
|
|
1067
|
+
if (eventType === "payment_intent.succeeded") status = "succeeded";
|
|
1068
|
+
if (eventType === "payment_intent.canceled") status = "canceled";
|
|
1069
|
+
if (eventType === "payment_intent.payment_failed") status = error?.code === "authentication_required" ? "requires_action" : "failed";
|
|
1070
|
+
const currency = paymentIntent.currency ? paymentIntent.currency.toUpperCase() : void 0;
|
|
1071
|
+
const metadata = { ...paymentIntent.metadata || {} };
|
|
1072
|
+
return {
|
|
1073
|
+
status,
|
|
1074
|
+
paymentExternalId: paymentIntent.id,
|
|
1075
|
+
customerExternalId: idOf(paymentIntent.customer),
|
|
1076
|
+
amountMinor: currency && typeof paymentIntent.amount === "number" ? stripeAmountToMinorUnitsOrUndefined(paymentIntent.amount, currency) : void 0,
|
|
1077
|
+
currency,
|
|
1078
|
+
chargeKey: metadata[CHARGE_KEY_METADATA],
|
|
1079
|
+
failureCode: status === "succeeded" ? void 0 : error?.code || void 0,
|
|
1080
|
+
declineCode: status === "succeeded" ? void 0 : error?.decline_code || void 0,
|
|
1081
|
+
failureMessage: status === "succeeded" ? void 0 : error?.message || void 0,
|
|
1082
|
+
metadata
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function idOf(value) {
|
|
1086
|
+
if (typeof value === "string") return value || void 0;
|
|
1087
|
+
return value?.id || void 0;
|
|
1088
|
+
}
|
|
1089
|
+
function oneOf(value, allowed) {
|
|
1090
|
+
return allowed.find((candidate) => candidate === value);
|
|
1091
|
+
}
|
|
1092
|
+
function isRecord(value) {
|
|
1093
|
+
return typeof value === "object" && value !== null;
|
|
1094
|
+
}
|
|
1095
|
+
function normalizeCurrencyCode(currency) {
|
|
1096
|
+
const code = String(currency ?? "").trim().toUpperCase();
|
|
1097
|
+
if (!/^[A-Z]{3}$/.test(code)) throw new Error(`Currency '${currency}' must be a three-letter code`);
|
|
1098
|
+
return code;
|
|
1099
|
+
}
|
|
1100
|
+
/** ISO 4217 minor-unit exponents other than 2. */
|
|
1101
|
+
var ISO_ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
1102
|
+
"BIF",
|
|
1103
|
+
"CLP",
|
|
1104
|
+
"DJF",
|
|
1105
|
+
"GNF",
|
|
1106
|
+
"ISK",
|
|
1107
|
+
"JPY",
|
|
1108
|
+
"KMF",
|
|
1109
|
+
"KRW",
|
|
1110
|
+
"PYG",
|
|
1111
|
+
"RWF",
|
|
1112
|
+
"UGX",
|
|
1113
|
+
"UYI",
|
|
1114
|
+
"VND",
|
|
1115
|
+
"VUV",
|
|
1116
|
+
"XAF",
|
|
1117
|
+
"XOF",
|
|
1118
|
+
"XPF"
|
|
1119
|
+
]);
|
|
1120
|
+
var ISO_THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
1121
|
+
"BHD",
|
|
1122
|
+
"IQD",
|
|
1123
|
+
"JOD",
|
|
1124
|
+
"KWD",
|
|
1125
|
+
"LYD",
|
|
1126
|
+
"OMR",
|
|
1127
|
+
"TND"
|
|
1128
|
+
]);
|
|
1129
|
+
var ISO_FOUR_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["CLF", "UYW"]);
|
|
1130
|
+
/**
|
|
1131
|
+
* ISO 4217 minor-unit exponent (2 for USD, 0 for JPY and ISK), from the
|
|
1132
|
+
* explicit tables above rather than the runtime's `Intl`/CLDR display digits,
|
|
1133
|
+
* which differ for some currencies (for example RSD, IQD, MGA) and change
|
|
1134
|
+
* between ICU releases.
|
|
1135
|
+
*/
|
|
1136
|
+
function isoMinorUnitExponent(currency) {
|
|
1137
|
+
if (ISO_ZERO_DECIMAL_CURRENCIES.has(currency)) return 0;
|
|
1138
|
+
if (ISO_THREE_DECIMAL_CURRENCIES.has(currency)) return 3;
|
|
1139
|
+
if (ISO_FOUR_DECIMAL_CURRENCIES.has(currency)) return 4;
|
|
1140
|
+
return 2;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Integer ISO minor units → Stripe's smallest unit. They differ where Stripe
|
|
1144
|
+
* keeps a two-decimal representation for a zero-decimal currency (ISK, UGX).
|
|
1145
|
+
*/
|
|
1146
|
+
function minorUnitsToStripeAmount(amountMinor, currency) {
|
|
1147
|
+
if (!Number.isSafeInteger(amountMinor)) throw new Error(`Amount ${amountMinor} must be a safe integer number of minor units`);
|
|
1148
|
+
const iso = 10 ** isoMinorUnitExponent(currency);
|
|
1149
|
+
const stripe = stripeCurrencyMinorUnitFactor(currency);
|
|
1150
|
+
if (stripe >= iso) return amountMinor * (stripe / iso);
|
|
1151
|
+
const divisor = iso / stripe;
|
|
1152
|
+
if (amountMinor % divisor !== 0) throw new Error(`Amount ${amountMinor} ${currency} minor units is not representable in Stripe's currency unit`);
|
|
1153
|
+
return amountMinor / divisor;
|
|
1154
|
+
}
|
|
1155
|
+
function stripeAmountToMinorUnitsOrUndefined(amount, currency) {
|
|
1156
|
+
if (!Number.isSafeInteger(amount)) return;
|
|
1157
|
+
const iso = 10 ** isoMinorUnitExponent(currency);
|
|
1158
|
+
const stripe = stripeCurrencyMinorUnitFactor(currency);
|
|
1159
|
+
const minor = amount * iso / stripe;
|
|
1160
|
+
return Number.isSafeInteger(minor) ? minor : void 0;
|
|
1161
|
+
}
|
|
718
1162
|
function toUnixSeconds(date) {
|
|
719
1163
|
return Math.floor(date.getTime() / 1e3);
|
|
720
1164
|
}
|
|
@@ -727,4 +1171,4 @@ function sleep(ms) {
|
|
|
727
1171
|
//#endregion
|
|
728
1172
|
export { StripeProvider };
|
|
729
1173
|
|
|
730
|
-
//# sourceMappingURL=stripe-
|
|
1174
|
+
//# sourceMappingURL=stripe-Dc061lPD.js.map
|