@happyvertical/accounting 0.91.0 → 0.93.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 +150 -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-DvSZ3fGk.js} +453 -34
- package/dist/chunks/stripe-DvSZ3fGk.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 +257 -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,35 @@ 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` });
|
|
182
280
|
}
|
|
183
281
|
async void(externalId) {
|
|
184
282
|
await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(externalId)}/void`);
|
|
@@ -227,6 +325,79 @@ var StripePaymentOperations = class {
|
|
|
227
325
|
async list(options = {}) {
|
|
228
326
|
return (await this.provider.request("GET", "/v1/payment_intents", mapListOptions(options))).data.map(mapStripePaymentIntent);
|
|
229
327
|
}
|
|
328
|
+
async chargeSavedPaymentMethod(input) {
|
|
329
|
+
assertIdempotencyKey(input.idempotencyKey);
|
|
330
|
+
if (!input.customerExternalId) throw new Error("Saved payment method charges require a customer");
|
|
331
|
+
const currency = normalizeCurrencyCode(input.currency);
|
|
332
|
+
const stripeAmount = minorUnitsToStripeAmount(input.amountMinor, currency);
|
|
333
|
+
if (stripeAmount <= 0) throw new Error("Charge amount must be a positive integer of minor units");
|
|
334
|
+
const chargeKey = input.idempotencyKey;
|
|
335
|
+
const base = {
|
|
336
|
+
provider: "stripe",
|
|
337
|
+
customerExternalId: input.customerExternalId,
|
|
338
|
+
amountMinor: input.amountMinor,
|
|
339
|
+
currency,
|
|
340
|
+
chargeKey
|
|
341
|
+
};
|
|
342
|
+
const existing = await this.findByChargeKey(chargeKey);
|
|
343
|
+
if (existing) {
|
|
344
|
+
assertSameCharge(existing, input.customerExternalId, stripeAmount, currency, input.paymentMethodExternalId);
|
|
345
|
+
return {
|
|
346
|
+
...base,
|
|
347
|
+
...mapChargeOutcome(existing)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
let paymentMethod = input.paymentMethodExternalId;
|
|
351
|
+
if (!paymentMethod) {
|
|
352
|
+
paymentMethod = idOf((await this.provider.request("GET", `/v1/customers/${encodeURIComponent(input.customerExternalId)}`)).invoice_settings?.default_payment_method);
|
|
353
|
+
if (!paymentMethod) return {
|
|
354
|
+
...base,
|
|
355
|
+
status: "failed",
|
|
356
|
+
failureCode: "payment_method_missing",
|
|
357
|
+
failureMessage: "The customer has no default payment method."
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const paymentIntent = await this.provider.request("POST", "/v1/payment_intents", {
|
|
362
|
+
amount: stripeAmount,
|
|
363
|
+
currency: currency.toLowerCase(),
|
|
364
|
+
customer: input.customerExternalId,
|
|
365
|
+
payment_method: paymentMethod,
|
|
366
|
+
off_session: true,
|
|
367
|
+
confirm: true,
|
|
368
|
+
description: input.description,
|
|
369
|
+
metadata: normalizeMetadata({
|
|
370
|
+
...input.metadata,
|
|
371
|
+
[CHARGE_KEY_METADATA]: chargeKey
|
|
372
|
+
})
|
|
373
|
+
}, { idempotencyKey: `${chargeKey}:payment_intent` });
|
|
374
|
+
return {
|
|
375
|
+
...base,
|
|
376
|
+
...mapChargeOutcome(paymentIntent)
|
|
377
|
+
};
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error instanceof StripeApiError && error.status === 402 && error.type === "card_error") {
|
|
380
|
+
const paymentIntent = isRecord(error.paymentIntent) ? error.paymentIntent : void 0;
|
|
381
|
+
return {
|
|
382
|
+
...base,
|
|
383
|
+
...paymentIntent ? mapChargeOutcome(paymentIntent) : {},
|
|
384
|
+
paymentMethodExternalId: idOf(paymentIntent?.payment_method) ?? paymentMethod,
|
|
385
|
+
status: error.code === "authentication_required" ? "requires_action" : "failed",
|
|
386
|
+
failureCode: error.code,
|
|
387
|
+
declineCode: error.declineCode,
|
|
388
|
+
failureMessage: error.stripeMessage,
|
|
389
|
+
raw: paymentIntent ? withoutClientSecret(paymentIntent) : void 0
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async findByChargeKey(chargeKey) {
|
|
396
|
+
return (await this.provider.request("GET", "/v1/payment_intents/search", {
|
|
397
|
+
query: `metadata['${CHARGE_KEY_METADATA}']:'${escapeSearchValue(chargeKey)}'`,
|
|
398
|
+
limit: 10
|
|
399
|
+
})).data.filter((intent) => intent.metadata?.[CHARGE_KEY_METADATA] === chargeKey).sort((left, right) => (left.created ?? 0) - (right.created ?? 0) || left.id.localeCompare(right.id))[0];
|
|
400
|
+
}
|
|
230
401
|
};
|
|
231
402
|
var StripeBillingOperationsImpl = class {
|
|
232
403
|
provider;
|
|
@@ -234,16 +405,38 @@ var StripeBillingOperationsImpl = class {
|
|
|
234
405
|
this.provider = provider;
|
|
235
406
|
}
|
|
236
407
|
async createCheckoutSession(input) {
|
|
237
|
-
|
|
408
|
+
return mapCheckoutSessionResponse(await this.provider.request("POST", "/v1/checkout/sessions", mapCheckoutSessionToStripe(input), { idempotencyKey: input.idempotencyKey }));
|
|
409
|
+
}
|
|
410
|
+
async retrieveCheckoutSession(sessionExternalId) {
|
|
411
|
+
const session = await this.provider.request("GET", `/v1/checkout/sessions/${encodeURIComponent(sessionExternalId)}`, { expand: ["setup_intent", "payment_intent"] });
|
|
412
|
+
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;
|
|
413
|
+
const currency = session.currency ? session.currency.toUpperCase() : void 0;
|
|
414
|
+
const minor = (amount) => currency && typeof amount === "number" ? stripeAmountToMinorUnitsOrUndefined(amount, currency) : void 0;
|
|
238
415
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
416
|
+
...mapCheckoutSessionResponse(session),
|
|
417
|
+
raw: withoutClientSecrets(session),
|
|
418
|
+
status: oneOf(session.status, [
|
|
419
|
+
"open",
|
|
420
|
+
"complete",
|
|
421
|
+
"expired"
|
|
422
|
+
]),
|
|
423
|
+
paymentStatus: oneOf(session.payment_status, [
|
|
424
|
+
"paid",
|
|
425
|
+
"unpaid",
|
|
426
|
+
"no_payment_required"
|
|
427
|
+
]),
|
|
428
|
+
paymentMethodExternalId: idOf(intent?.payment_method),
|
|
429
|
+
currency,
|
|
430
|
+
amountSubtotalMinor: minor(session.amount_subtotal),
|
|
431
|
+
amountTaxMinor: minor(session.total_details?.amount_tax),
|
|
432
|
+
amountTotalMinor: minor(session.amount_total),
|
|
433
|
+
metadata: { ...session.metadata || {} }
|
|
245
434
|
};
|
|
246
435
|
}
|
|
436
|
+
async setDefaultPaymentMethod(customerExternalId, paymentMethodExternalId) {
|
|
437
|
+
if (!customerExternalId || !paymentMethodExternalId) throw new Error("setDefaultPaymentMethod requires a customer and a payment method");
|
|
438
|
+
await this.provider.request("POST", `/v1/customers/${encodeURIComponent(customerExternalId)}`, { invoice_settings: { default_payment_method: paymentMethodExternalId } });
|
|
439
|
+
}
|
|
247
440
|
async createCustomerPortalSession(input) {
|
|
248
441
|
const response = await this.provider.request("POST", "/v1/billing_portal/sessions", {
|
|
249
442
|
customer: input.customerExternalId,
|
|
@@ -350,7 +543,9 @@ var StripeWebhookOperations = class {
|
|
|
350
543
|
throw new Error(`Invalid Stripe webhook payload: ${message}`);
|
|
351
544
|
}
|
|
352
545
|
const resource = event?.data?.object;
|
|
546
|
+
const payment = resource?.object === "payment_intent" && typeof resource.id === "string" ? mapWebhookPayment(resource, event?.type || void 0) : void 0;
|
|
353
547
|
return {
|
|
548
|
+
...payment ? { payment } : {},
|
|
354
549
|
type: event?.type || "unknown",
|
|
355
550
|
id: typeof event?.id === "string" ? event.id : void 0,
|
|
356
551
|
provider: "stripe",
|
|
@@ -398,19 +593,19 @@ function mapCustomerToStripe(customer) {
|
|
|
398
593
|
country: customer.billingAddress.country
|
|
399
594
|
} : void 0,
|
|
400
595
|
metadata: normalizeMetadata({
|
|
401
|
-
local_id: customer.id,
|
|
402
596
|
payment_terms: customer.paymentTerms,
|
|
403
|
-
...customer.metadata
|
|
597
|
+
...customer.metadata,
|
|
598
|
+
local_id: customer.id
|
|
404
599
|
})
|
|
405
600
|
};
|
|
406
601
|
if (customer.taxExempt !== void 0) stripeCustomer.tax_exempt = customer.taxExempt ? "exempt" : "none";
|
|
407
602
|
return stripeCustomer;
|
|
408
603
|
}
|
|
409
|
-
function mapInvoiceToStripe(invoice) {
|
|
604
|
+
function mapInvoiceToStripe(invoice, collectionMethod) {
|
|
410
605
|
return {
|
|
411
606
|
customer: invoice.customerExternalId,
|
|
412
|
-
collection_method:
|
|
413
|
-
due_date: invoice.dueDate,
|
|
607
|
+
collection_method: collectionMethod,
|
|
608
|
+
due_date: collectionMethod === "send_invoice" ? invoice.dueDate : void 0,
|
|
414
609
|
auto_advance: false,
|
|
415
610
|
pending_invoice_items_behavior: "include",
|
|
416
611
|
automatic_tax: invoice.automaticTax ? { enabled: true } : void 0,
|
|
@@ -426,9 +621,10 @@ function mapInvoiceToStripe(invoice) {
|
|
|
426
621
|
})
|
|
427
622
|
};
|
|
428
623
|
}
|
|
429
|
-
function mapInvoiceUpdateToStripe(invoice) {
|
|
624
|
+
function mapInvoiceUpdateToStripe(invoice, collectionMethod) {
|
|
430
625
|
return {
|
|
431
|
-
|
|
626
|
+
collection_method: invoice.collectionMethod ? collectionMethod : void 0,
|
|
627
|
+
due_date: collectionMethod === "send_invoice" ? invoice.dueDate : void 0,
|
|
432
628
|
metadata: normalizeMetadata({
|
|
433
629
|
local_id: invoice.id,
|
|
434
630
|
invoice_number: invoice.invoiceNumber,
|
|
@@ -439,35 +635,76 @@ function mapInvoiceUpdateToStripe(invoice) {
|
|
|
439
635
|
};
|
|
440
636
|
}
|
|
441
637
|
function mapCheckoutSessionToStripe(input) {
|
|
638
|
+
const mode = input.mode || "subscription";
|
|
639
|
+
const lineItems = input.lineItems || [];
|
|
640
|
+
if (mode === "setup") {
|
|
641
|
+
if (lineItems.length > 0) throw new Error("Stripe setup-mode Checkout does not accept line items");
|
|
642
|
+
if (!input.currency && !input.paymentMethodTypes?.length) throw new Error("Stripe setup-mode Checkout requires currency or paymentMethodTypes");
|
|
643
|
+
if (input.automaticTax) throw new Error("Stripe setup-mode Checkout does not calculate tax");
|
|
644
|
+
} else if (lineItems.length === 0) throw new Error(`Stripe ${mode}-mode Checkout requires line items`);
|
|
645
|
+
if (input.setupFutureUsage && mode !== "payment") throw new Error("setupFutureUsage applies only to payment-mode Checkout");
|
|
646
|
+
if (input.customerUpdate && !input.customerExternalId) throw new Error("customerUpdate requires customerExternalId");
|
|
442
647
|
return {
|
|
443
|
-
mode
|
|
648
|
+
mode,
|
|
444
649
|
success_url: input.successUrl,
|
|
445
650
|
cancel_url: input.cancelUrl,
|
|
446
651
|
customer: input.customerExternalId,
|
|
447
652
|
customer_email: input.customerExternalId ? void 0 : input.customerEmail,
|
|
448
653
|
client_reference_id: input.clientReferenceId,
|
|
449
654
|
allow_promotion_codes: input.allowPromotionCodes,
|
|
450
|
-
|
|
655
|
+
currency: input.currency ? input.currency.toLowerCase() : void 0,
|
|
656
|
+
payment_method_types: input.paymentMethodTypes,
|
|
657
|
+
automatic_tax: input.automaticTax ? { enabled: true } : void 0,
|
|
658
|
+
billing_address_collection: input.billingAddressCollection,
|
|
659
|
+
customer_update: input.customerUpdate ? {
|
|
660
|
+
address: input.customerUpdate.address,
|
|
661
|
+
name: input.customerUpdate.name,
|
|
662
|
+
shipping: input.customerUpdate.shipping
|
|
663
|
+
} : void 0,
|
|
664
|
+
payment_intent_data: input.setupFutureUsage ? { setup_future_usage: input.setupFutureUsage } : void 0,
|
|
665
|
+
line_items: mode === "setup" ? void 0 : lineItems.map((lineItem) => mapCheckoutLineItem(lineItem, Boolean(input.automaticTax))),
|
|
451
666
|
metadata: normalizeMetadata(input.metadata || {})
|
|
452
667
|
};
|
|
453
668
|
}
|
|
454
|
-
function
|
|
669
|
+
function mapCheckoutSessionResponse(response) {
|
|
670
|
+
return {
|
|
671
|
+
externalId: response.id,
|
|
672
|
+
url: response.url || null,
|
|
673
|
+
mode: oneOf(response.mode, [
|
|
674
|
+
"payment",
|
|
675
|
+
"setup",
|
|
676
|
+
"subscription"
|
|
677
|
+
]),
|
|
678
|
+
customerExternalId: idOf(response.customer),
|
|
679
|
+
subscriptionExternalId: idOf(response.subscription),
|
|
680
|
+
paymentIntentExternalId: idOf(response.payment_intent),
|
|
681
|
+
setupIntentExternalId: idOf(response.setup_intent),
|
|
682
|
+
raw: withoutClientSecrets(response)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function mapCheckoutLineItem(lineItem, automaticTax = false) {
|
|
455
686
|
if (lineItem.price) return {
|
|
456
687
|
price: lineItem.price,
|
|
457
688
|
quantity: lineItem.quantity || 1
|
|
458
689
|
};
|
|
459
690
|
if (!lineItem.priceData) throw new Error("Stripe checkout line item requires price or priceData");
|
|
460
|
-
const
|
|
691
|
+
const { priceData } = lineItem;
|
|
692
|
+
const productData = priceData.product ? void 0 : { name: priceData.productName || "Subscription" };
|
|
693
|
+
const hasStripeAmount = priceData.unitAmount !== void 0;
|
|
694
|
+
const hasMinorAmount = priceData.unitAmountMinor !== void 0;
|
|
695
|
+
if (hasStripeAmount === hasMinorAmount) throw new Error("Stripe checkout priceData requires exactly one of unitAmount or unitAmountMinor");
|
|
696
|
+
const unitAmount = hasMinorAmount ? minorUnitsToStripeAmount(priceData.unitAmountMinor, normalizeCurrencyCode(priceData.currency)) : priceData.unitAmount;
|
|
461
697
|
return {
|
|
462
698
|
quantity: lineItem.quantity || 1,
|
|
463
699
|
price_data: {
|
|
464
|
-
currency:
|
|
465
|
-
unit_amount:
|
|
466
|
-
|
|
700
|
+
currency: priceData.currency.toLowerCase(),
|
|
701
|
+
unit_amount: unitAmount,
|
|
702
|
+
tax_behavior: priceData.taxBehavior ?? (automaticTax ? "exclusive" : void 0),
|
|
703
|
+
product: priceData.product,
|
|
467
704
|
product_data: productData,
|
|
468
|
-
recurring:
|
|
469
|
-
interval:
|
|
470
|
-
interval_count:
|
|
705
|
+
recurring: priceData.recurring ? {
|
|
706
|
+
interval: priceData.recurring.interval,
|
|
707
|
+
interval_count: priceData.recurring.intervalCount
|
|
471
708
|
} : void 0
|
|
472
709
|
}
|
|
473
710
|
};
|
|
@@ -556,6 +793,7 @@ function mapStripeInvoiceStatus(status, dueDate) {
|
|
|
556
793
|
case "draft": return "draft";
|
|
557
794
|
case "paid": return "paid";
|
|
558
795
|
case "void": return "voided";
|
|
796
|
+
case "uncollectible": return "uncollectible";
|
|
559
797
|
case "open":
|
|
560
798
|
if (dueDate && dueDate * 1e3 < Date.now()) return "overdue";
|
|
561
799
|
return "sent";
|
|
@@ -715,6 +953,187 @@ function moneyToStripeMinorUnits(amount, currency) {
|
|
|
715
953
|
function stripeMinorUnitsToMoney(amount, currency) {
|
|
716
954
|
return amount / stripeCurrencyMinorUnitFactor(currency);
|
|
717
955
|
}
|
|
956
|
+
var CHARGE_KEY_METADATA = "hv_charge_key";
|
|
957
|
+
function assertIdempotencyKey(key) {
|
|
958
|
+
if (typeof key !== "string" || key.trim() === "") throw new Error("idempotencyKey must be a non-empty string");
|
|
959
|
+
}
|
|
960
|
+
/** Escape a value for a single-quoted Stripe Search Query Language string. */
|
|
961
|
+
function escapeSearchValue(value) {
|
|
962
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
963
|
+
}
|
|
964
|
+
function resolveCollectionMethod(invoice) {
|
|
965
|
+
const method = invoice.collectionMethod ?? "send_invoice";
|
|
966
|
+
if (method !== "send_invoice" && method !== "charge_automatically") throw new Error(`Unsupported invoice collectionMethod '${method}'`);
|
|
967
|
+
return method;
|
|
968
|
+
}
|
|
969
|
+
/** The line's discount in Stripe units, validated against the line total. */
|
|
970
|
+
function lineDiscountToStripe(lineItem, currency) {
|
|
971
|
+
if (lineItem.discount === void 0 || lineItem.discount === 0) return;
|
|
972
|
+
if (!Number.isFinite(lineItem.discount) || lineItem.discount < 0) throw new Error(`Invoice line '${lineItem.description}' discount must be a non-negative amount`);
|
|
973
|
+
const discount = moneyToStripeMinorUnits(lineItem.discount, currency);
|
|
974
|
+
if (discount > Math.round(moneyToStripeMinorUnits(lineItem.unitPrice, currency) * lineItem.quantity)) throw new Error(`Invoice line '${lineItem.description}' discount exceeds the line total`);
|
|
975
|
+
return discount > 0 ? discount : void 0;
|
|
976
|
+
}
|
|
977
|
+
function lineServicePeriod(lineItem) {
|
|
978
|
+
const { periodStart, periodEnd } = lineItem;
|
|
979
|
+
if (!periodStart && !periodEnd) return;
|
|
980
|
+
if (!(periodStart instanceof Date) || !(periodEnd instanceof Date)) throw new Error(`Invoice line '${lineItem.description}' needs both periodStart and periodEnd`);
|
|
981
|
+
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`);
|
|
982
|
+
return {
|
|
983
|
+
start: periodStart,
|
|
984
|
+
end: periodEnd
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function mapChargeOutcome(paymentIntent) {
|
|
988
|
+
const error = paymentIntent.last_payment_error;
|
|
989
|
+
const status = mapPaymentIntentChargeStatus(paymentIntent.status, error?.code);
|
|
990
|
+
return {
|
|
991
|
+
status,
|
|
992
|
+
paymentExternalId: paymentIntent.id,
|
|
993
|
+
paymentMethodExternalId: idOf(paymentIntent.payment_method),
|
|
994
|
+
failureCode: status === "succeeded" ? void 0 : error?.code || void 0,
|
|
995
|
+
declineCode: status === "succeeded" ? void 0 : error?.decline_code || void 0,
|
|
996
|
+
failureMessage: status === "succeeded" ? void 0 : error?.message || void 0,
|
|
997
|
+
raw: withoutClientSecret(paymentIntent)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* A PaymentIntent found by charge key after Stripe's idempotency window must
|
|
1002
|
+
* be the charge this call describes; a key reused for a different amount,
|
|
1003
|
+
* currency, or customer is refused instead of reporting the wrong charge.
|
|
1004
|
+
*/
|
|
1005
|
+
function assertSameCharge(paymentIntent, customerExternalId, stripeAmount, currency, paymentMethodExternalId) {
|
|
1006
|
+
const customer = idOf(paymentIntent.customer);
|
|
1007
|
+
const paymentMethod = idOf(paymentIntent.payment_method);
|
|
1008
|
+
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`);
|
|
1009
|
+
}
|
|
1010
|
+
/** Remove `client_secret` from a Stripe object and its expanded intents. */
|
|
1011
|
+
function withoutClientSecrets(value) {
|
|
1012
|
+
const copy = withoutClientSecret(value);
|
|
1013
|
+
for (const key of ["payment_intent", "setup_intent"]) {
|
|
1014
|
+
const nested = copy[key];
|
|
1015
|
+
if (isRecord(nested)) copy[key] = withoutClientSecret(nested);
|
|
1016
|
+
}
|
|
1017
|
+
return copy;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Drop the intent client secret before handing objects to callers, including
|
|
1021
|
+
* `next_action`, whose redirect URL and SDK payload embed the same secret.
|
|
1022
|
+
*/
|
|
1023
|
+
function withoutClientSecret(value) {
|
|
1024
|
+
const { client_secret: _secret, next_action: _nextAction, ...rest } = value;
|
|
1025
|
+
return rest;
|
|
1026
|
+
}
|
|
1027
|
+
function mapPaymentIntentChargeStatus(status, errorCode) {
|
|
1028
|
+
switch (status) {
|
|
1029
|
+
case "succeeded": return "succeeded";
|
|
1030
|
+
case "processing":
|
|
1031
|
+
case "requires_capture": return "processing";
|
|
1032
|
+
case "requires_action":
|
|
1033
|
+
case "requires_confirmation": return "requires_action";
|
|
1034
|
+
case "canceled": return "canceled";
|
|
1035
|
+
case "requires_payment_method": return errorCode === "authentication_required" ? "requires_action" : "failed";
|
|
1036
|
+
default: return "processing";
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function mapWebhookPayment(paymentIntent, eventType) {
|
|
1040
|
+
const error = paymentIntent.last_payment_error;
|
|
1041
|
+
let status = mapPaymentIntentChargeStatus(paymentIntent.status, error?.code);
|
|
1042
|
+
if (eventType === "payment_intent.succeeded") status = "succeeded";
|
|
1043
|
+
if (eventType === "payment_intent.canceled") status = "canceled";
|
|
1044
|
+
if (eventType === "payment_intent.payment_failed") status = error?.code === "authentication_required" ? "requires_action" : "failed";
|
|
1045
|
+
const currency = paymentIntent.currency ? paymentIntent.currency.toUpperCase() : void 0;
|
|
1046
|
+
const metadata = { ...paymentIntent.metadata || {} };
|
|
1047
|
+
return {
|
|
1048
|
+
status,
|
|
1049
|
+
paymentExternalId: paymentIntent.id,
|
|
1050
|
+
customerExternalId: idOf(paymentIntent.customer),
|
|
1051
|
+
amountMinor: currency && typeof paymentIntent.amount === "number" ? stripeAmountToMinorUnitsOrUndefined(paymentIntent.amount, currency) : void 0,
|
|
1052
|
+
currency,
|
|
1053
|
+
chargeKey: metadata[CHARGE_KEY_METADATA],
|
|
1054
|
+
failureCode: status === "succeeded" ? void 0 : error?.code || void 0,
|
|
1055
|
+
declineCode: status === "succeeded" ? void 0 : error?.decline_code || void 0,
|
|
1056
|
+
failureMessage: status === "succeeded" ? void 0 : error?.message || void 0,
|
|
1057
|
+
metadata
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function idOf(value) {
|
|
1061
|
+
if (typeof value === "string") return value || void 0;
|
|
1062
|
+
return value?.id || void 0;
|
|
1063
|
+
}
|
|
1064
|
+
function oneOf(value, allowed) {
|
|
1065
|
+
return allowed.find((candidate) => candidate === value);
|
|
1066
|
+
}
|
|
1067
|
+
function isRecord(value) {
|
|
1068
|
+
return typeof value === "object" && value !== null;
|
|
1069
|
+
}
|
|
1070
|
+
function normalizeCurrencyCode(currency) {
|
|
1071
|
+
const code = String(currency ?? "").trim().toUpperCase();
|
|
1072
|
+
if (!/^[A-Z]{3}$/.test(code)) throw new Error(`Currency '${currency}' must be a three-letter code`);
|
|
1073
|
+
return code;
|
|
1074
|
+
}
|
|
1075
|
+
/** ISO 4217 minor-unit exponents other than 2. */
|
|
1076
|
+
var ISO_ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
1077
|
+
"BIF",
|
|
1078
|
+
"CLP",
|
|
1079
|
+
"DJF",
|
|
1080
|
+
"GNF",
|
|
1081
|
+
"ISK",
|
|
1082
|
+
"JPY",
|
|
1083
|
+
"KMF",
|
|
1084
|
+
"KRW",
|
|
1085
|
+
"PYG",
|
|
1086
|
+
"RWF",
|
|
1087
|
+
"UGX",
|
|
1088
|
+
"UYI",
|
|
1089
|
+
"VND",
|
|
1090
|
+
"VUV",
|
|
1091
|
+
"XAF",
|
|
1092
|
+
"XOF",
|
|
1093
|
+
"XPF"
|
|
1094
|
+
]);
|
|
1095
|
+
var ISO_THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
1096
|
+
"BHD",
|
|
1097
|
+
"IQD",
|
|
1098
|
+
"JOD",
|
|
1099
|
+
"KWD",
|
|
1100
|
+
"LYD",
|
|
1101
|
+
"OMR",
|
|
1102
|
+
"TND"
|
|
1103
|
+
]);
|
|
1104
|
+
var ISO_FOUR_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["CLF", "UYW"]);
|
|
1105
|
+
/**
|
|
1106
|
+
* ISO 4217 minor-unit exponent (2 for USD, 0 for JPY and ISK), from the
|
|
1107
|
+
* explicit tables above rather than the runtime's `Intl`/CLDR display digits,
|
|
1108
|
+
* which differ for some currencies (for example RSD, IQD, MGA) and change
|
|
1109
|
+
* between ICU releases.
|
|
1110
|
+
*/
|
|
1111
|
+
function isoMinorUnitExponent(currency) {
|
|
1112
|
+
if (ISO_ZERO_DECIMAL_CURRENCIES.has(currency)) return 0;
|
|
1113
|
+
if (ISO_THREE_DECIMAL_CURRENCIES.has(currency)) return 3;
|
|
1114
|
+
if (ISO_FOUR_DECIMAL_CURRENCIES.has(currency)) return 4;
|
|
1115
|
+
return 2;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Integer ISO minor units → Stripe's smallest unit. They differ where Stripe
|
|
1119
|
+
* keeps a two-decimal representation for a zero-decimal currency (ISK, UGX).
|
|
1120
|
+
*/
|
|
1121
|
+
function minorUnitsToStripeAmount(amountMinor, currency) {
|
|
1122
|
+
if (!Number.isSafeInteger(amountMinor)) throw new Error(`Amount ${amountMinor} must be a safe integer number of minor units`);
|
|
1123
|
+
const iso = 10 ** isoMinorUnitExponent(currency);
|
|
1124
|
+
const stripe = stripeCurrencyMinorUnitFactor(currency);
|
|
1125
|
+
if (stripe >= iso) return amountMinor * (stripe / iso);
|
|
1126
|
+
const divisor = iso / stripe;
|
|
1127
|
+
if (amountMinor % divisor !== 0) throw new Error(`Amount ${amountMinor} ${currency} minor units is not representable in Stripe's currency unit`);
|
|
1128
|
+
return amountMinor / divisor;
|
|
1129
|
+
}
|
|
1130
|
+
function stripeAmountToMinorUnitsOrUndefined(amount, currency) {
|
|
1131
|
+
if (!Number.isSafeInteger(amount)) return;
|
|
1132
|
+
const iso = 10 ** isoMinorUnitExponent(currency);
|
|
1133
|
+
const stripe = stripeCurrencyMinorUnitFactor(currency);
|
|
1134
|
+
const minor = amount * iso / stripe;
|
|
1135
|
+
return Number.isSafeInteger(minor) ? minor : void 0;
|
|
1136
|
+
}
|
|
718
1137
|
function toUnixSeconds(date) {
|
|
719
1138
|
return Math.floor(date.getTime() / 1e3);
|
|
720
1139
|
}
|
|
@@ -727,4 +1146,4 @@ function sleep(ms) {
|
|
|
727
1146
|
//#endregion
|
|
728
1147
|
export { StripeProvider };
|
|
729
1148
|
|
|
730
|
-
//# sourceMappingURL=stripe-
|
|
1149
|
+
//# sourceMappingURL=stripe-DvSZ3fGk.js.map
|