@happyvertical/accounting 0.79.0 → 0.80.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.
@@ -0,0 +1,644 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+ //#region src/providers/stripe/index.ts
3
+ /**
4
+ * Stripe Provider
5
+ *
6
+ * Implements Stripe AR, payment, webhook, and billing operations using the
7
+ * Stripe HTTP API directly. The SDK keeps Stripe as an optional runtime
8
+ * dependency by relying on the platform fetch implementation.
9
+ */
10
+ /**
11
+ * Stripe accounting provider.
12
+ */
13
+ var StripeProvider = class {
14
+ type = "stripe";
15
+ options;
16
+ fetchImpl;
17
+ customers;
18
+ invoices;
19
+ vendors;
20
+ bills;
21
+ payments;
22
+ audit;
23
+ webhooks;
24
+ billing;
25
+ constructor(options) {
26
+ this.options = {
27
+ timeout: 3e4,
28
+ maxRetries: 3,
29
+ apiBaseUrl: "https://api.stripe.com",
30
+ webhookTolerance: 300,
31
+ ...options
32
+ };
33
+ this.fetchImpl = options.fetch || fetch;
34
+ this.customers = new StripeCustomerOperations(this);
35
+ this.invoices = new StripeInvoiceOperations(this);
36
+ this.vendors = new UnsupportedVendorOperations();
37
+ this.bills = new UnsupportedBillOperations();
38
+ this.payments = new StripePaymentOperations(this);
39
+ this.audit = new StripeAuditOperations(this);
40
+ this.webhooks = new StripeWebhookOperations(this.options);
41
+ this.billing = new StripeBillingOperationsImpl(this);
42
+ }
43
+ async request(method, endpoint, params) {
44
+ const timeout = this.options.timeout || 3e4;
45
+ const maxRetries = this.options.maxRetries || 3;
46
+ const idempotencyKey = method === "POST" ? `sdk-${randomUUID()}` : null;
47
+ let lastError = null;
48
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
49
+ const controller = new AbortController();
50
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
51
+ try {
52
+ const url = this.buildUrl(endpoint, method === "GET" ? params : {});
53
+ const body = method === "POST" && params ? encodeStripeForm(params).toString() : void 0;
54
+ const headers = {
55
+ Authorization: `Bearer ${this.options.secretKey}`,
56
+ Accept: "application/json"
57
+ };
58
+ if (method === "POST") {
59
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
60
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
61
+ }
62
+ if (this.options.apiVersion) headers["Stripe-Version"] = this.options.apiVersion;
63
+ const response = await this.fetchImpl(url, {
64
+ method,
65
+ headers,
66
+ body,
67
+ signal: controller.signal
68
+ });
69
+ if (!response.ok) {
70
+ const errorText = await response.text();
71
+ const error = /* @__PURE__ */ new Error(`Stripe API error (${response.status}): ${errorText}`);
72
+ if (response.status < 500 && response.status !== 429) throw error;
73
+ lastError = error;
74
+ } else {
75
+ const text = await response.text();
76
+ return text ? JSON.parse(text) : {};
77
+ }
78
+ } catch (error) {
79
+ lastError = error instanceof Error ? error : new Error(String(error));
80
+ const statusMatch = lastError.message.match(/Stripe API error \((\d+)\)/);
81
+ if (statusMatch) {
82
+ const status = Number.parseInt(statusMatch[1], 10);
83
+ if (status >= 400 && status < 500 && status !== 429) throw lastError;
84
+ }
85
+ } finally {
86
+ clearTimeout(timeoutId);
87
+ }
88
+ if (attempt < maxRetries) await sleep(2 ** attempt * 500);
89
+ }
90
+ throw lastError || /* @__PURE__ */ new Error("Stripe request failed after retries");
91
+ }
92
+ buildUrl(endpoint, params) {
93
+ const baseUrl = this.options.apiBaseUrl || "https://api.stripe.com";
94
+ const url = new URL(endpoint, baseUrl);
95
+ if (params && Object.keys(params).length > 0) for (const [key, value] of encodeStripeForm(params).entries()) url.searchParams.append(key, value);
96
+ return url.toString();
97
+ }
98
+ };
99
+ var StripeCustomerOperations = class {
100
+ provider;
101
+ constructor(provider) {
102
+ this.provider = provider;
103
+ }
104
+ async push(customer) {
105
+ return {
106
+ action: "created",
107
+ externalId: (await this.provider.request("POST", "/v1/customers", mapCustomerToStripe(customer))).id,
108
+ syncedAt: /* @__PURE__ */ new Date()
109
+ };
110
+ }
111
+ async pull(externalId) {
112
+ return mapStripeCustomer(await this.provider.request("GET", `/v1/customers/${encodeURIComponent(externalId)}`));
113
+ }
114
+ async list(options = {}) {
115
+ return (await this.provider.request("GET", "/v1/customers", mapListOptions(options))).data.map(mapStripeCustomer);
116
+ }
117
+ async sync(customer) {
118
+ if (!customer.externalId) return this.push(customer);
119
+ await this.provider.request("POST", `/v1/customers/${encodeURIComponent(customer.externalId)}`, mapCustomerToStripe(customer));
120
+ return {
121
+ action: "updated",
122
+ externalId: customer.externalId,
123
+ syncedAt: /* @__PURE__ */ new Date()
124
+ };
125
+ }
126
+ };
127
+ var StripeInvoiceOperations = class {
128
+ provider;
129
+ constructor(provider) {
130
+ this.provider = provider;
131
+ }
132
+ async push(invoice) {
133
+ if (!invoice.customerExternalId) throw new Error("Stripe invoices require customerExternalId");
134
+ for (const lineItem of invoice.lineItems) await this.provider.request("POST", "/v1/invoiceitems", {
135
+ customer: invoice.customerExternalId,
136
+ currency: invoice.currency || "usd",
137
+ description: lineItem.description,
138
+ quantity: lineItem.quantity,
139
+ unit_amount_decimal: String(Math.round(lineItem.unitPrice * 100)),
140
+ metadata: normalizeMetadata({
141
+ local_sku: lineItem.sku,
142
+ local_discount: lineItem.discount,
143
+ local_tax_rate: lineItem.taxRate
144
+ })
145
+ });
146
+ return {
147
+ action: "created",
148
+ externalId: (await this.provider.request("POST", "/v1/invoices", mapInvoiceToStripe(invoice))).id,
149
+ syncedAt: /* @__PURE__ */ new Date()
150
+ };
151
+ }
152
+ async pull(externalId) {
153
+ return mapStripeInvoice(await this.provider.request("GET", `/v1/invoices/${encodeURIComponent(externalId)}`));
154
+ }
155
+ async list(options = {}) {
156
+ return (await this.provider.request("GET", "/v1/invoices", mapListOptions(options))).data.map(mapStripeInvoice);
157
+ }
158
+ async sync(invoice) {
159
+ if (!invoice.externalId) return this.push(invoice);
160
+ await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(invoice.externalId)}`, mapInvoiceUpdateToStripe(invoice));
161
+ return {
162
+ action: "updated",
163
+ externalId: invoice.externalId,
164
+ syncedAt: /* @__PURE__ */ new Date()
165
+ };
166
+ }
167
+ async send(externalId) {
168
+ await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(externalId)}/send`);
169
+ }
170
+ async void(externalId) {
171
+ await this.provider.request("POST", `/v1/invoices/${encodeURIComponent(externalId)}/void`);
172
+ }
173
+ };
174
+ var StripePaymentOperations = class {
175
+ provider;
176
+ constructor(provider) {
177
+ this.provider = provider;
178
+ }
179
+ async pull(externalId) {
180
+ return mapStripePaymentIntent(await this.provider.request("GET", `/v1/payment_intents/${encodeURIComponent(externalId)}`));
181
+ }
182
+ async list(options = {}) {
183
+ return (await this.provider.request("GET", "/v1/payment_intents", mapListOptions(options))).data.map(mapStripePaymentIntent);
184
+ }
185
+ };
186
+ var StripeBillingOperationsImpl = class {
187
+ provider;
188
+ constructor(provider) {
189
+ this.provider = provider;
190
+ }
191
+ async createCheckoutSession(input) {
192
+ const response = await this.provider.request("POST", "/v1/checkout/sessions", mapCheckoutSessionToStripe(input));
193
+ return {
194
+ externalId: response.id,
195
+ url: response.url || null,
196
+ customerExternalId: response.customer || void 0,
197
+ subscriptionExternalId: response.subscription || void 0,
198
+ paymentIntentExternalId: response.payment_intent || void 0,
199
+ raw: response
200
+ };
201
+ }
202
+ async createCustomerPortalSession(input) {
203
+ const response = await this.provider.request("POST", "/v1/billing_portal/sessions", {
204
+ customer: input.customerExternalId,
205
+ return_url: input.returnUrl,
206
+ configuration: input.configurationExternalId
207
+ });
208
+ return {
209
+ externalId: response.id,
210
+ url: response.url,
211
+ raw: response
212
+ };
213
+ }
214
+ async retrieveSubscriptionStatus(subscriptionExternalId) {
215
+ return mapStripeSubscription(await this.provider.request("GET", `/v1/subscriptions/${encodeURIComponent(subscriptionExternalId)}`));
216
+ }
217
+ async listCustomerSubscriptions(customerExternalId) {
218
+ return (await this.provider.request("GET", "/v1/subscriptions", {
219
+ customer: customerExternalId,
220
+ status: "all",
221
+ limit: 100
222
+ })).data.map(mapStripeSubscription);
223
+ }
224
+ };
225
+ var UnsupportedVendorOperations = class {
226
+ async push(_vendor) {
227
+ throw new Error("Stripe does not support vendor operations");
228
+ }
229
+ async pull(_externalId) {
230
+ throw new Error("Stripe does not support vendor operations");
231
+ }
232
+ async list(_options) {
233
+ throw new Error("Stripe does not support vendor operations");
234
+ }
235
+ async sync(_vendor) {
236
+ throw new Error("Stripe does not support vendor operations");
237
+ }
238
+ };
239
+ var UnsupportedBillOperations = class {
240
+ async push(_bill) {
241
+ throw new Error("Stripe does not support bill operations");
242
+ }
243
+ async pull(_externalId) {
244
+ throw new Error("Stripe does not support bill operations");
245
+ }
246
+ async list(_options) {
247
+ throw new Error("Stripe does not support bill operations");
248
+ }
249
+ async sync(_bill) {
250
+ throw new Error("Stripe does not support bill operations");
251
+ }
252
+ };
253
+ var StripeAuditOperations = class {
254
+ provider;
255
+ constructor(provider) {
256
+ this.provider = provider;
257
+ }
258
+ async reconcileCustomers(locals) {
259
+ return reconcileRecords("stripe", locals, await this.provider.customers.list({ limit: 100 }), compareCustomer);
260
+ }
261
+ async reconcileInvoices(locals, dateRange) {
262
+ return reconcileRecords("stripe", locals, await this.provider.invoices.list({
263
+ limit: 100,
264
+ startDate: dateRange?.start,
265
+ endDate: dateRange?.end
266
+ }), compareInvoice);
267
+ }
268
+ async reconcileVendors(_locals) {
269
+ throw new Error("Stripe does not support vendor operations");
270
+ }
271
+ async reconcileBills(_locals) {
272
+ throw new Error("Stripe does not support bill operations");
273
+ }
274
+ async reconcilePayments(locals, dateRange) {
275
+ return reconcileRecords("stripe", locals, await this.provider.payments.list({
276
+ limit: 100,
277
+ startDate: dateRange?.start,
278
+ endDate: dateRange?.end
279
+ }), comparePayment);
280
+ }
281
+ };
282
+ var StripeWebhookOperations = class {
283
+ options;
284
+ constructor(options) {
285
+ this.options = options;
286
+ }
287
+ verify(payload, signature, secret) {
288
+ const endpointSecret = secret || this.options.webhookSecret;
289
+ if (!endpointSecret) throw new Error("Stripe webhook secret not provided. Pass it as the secret parameter or configure webhookSecret in options.");
290
+ const parsed = parseStripeSignatureHeader(signature);
291
+ if (!parsed.timestamp || parsed.signatures.length === 0) return false;
292
+ const tolerance = this.options.webhookTolerance ?? 300;
293
+ const timestamp = Number.parseInt(parsed.timestamp, 10);
294
+ if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1e3 - timestamp) > tolerance) return false;
295
+ const signedPayload = `${parsed.timestamp}.${payload}`;
296
+ const expected = createHmac("sha256", endpointSecret).update(signedPayload).digest("hex");
297
+ return parsed.signatures.some((value) => constantTimeEqualHex(value, expected));
298
+ }
299
+ parse(payload) {
300
+ let event;
301
+ try {
302
+ event = JSON.parse(payload);
303
+ } catch (error) {
304
+ const message = error instanceof Error ? error.message : "Unknown JSON parse error";
305
+ throw new Error(`Invalid Stripe webhook payload: ${message}`);
306
+ }
307
+ const resource = event?.data?.object;
308
+ return {
309
+ type: event?.type || "unknown",
310
+ provider: "stripe",
311
+ timestamp: unixToDate(event?.created) || /* @__PURE__ */ new Date(),
312
+ payload: event,
313
+ resourceType: mapStripeResourceType(resource?.object || void 0, event?.type || void 0),
314
+ resourceId: typeof resource?.id === "string" ? resource.id : void 0
315
+ };
316
+ }
317
+ };
318
+ function encodeStripeForm(params) {
319
+ const form = new URLSearchParams();
320
+ for (const [key, value] of Object.entries(params)) appendStripeFormValue(form, key, value);
321
+ return form;
322
+ }
323
+ function appendStripeFormValue(form, key, value) {
324
+ if (value === void 0 || value === null) return;
325
+ if (value instanceof Date) {
326
+ form.append(key, String(toUnixSeconds(value)));
327
+ return;
328
+ }
329
+ if (Array.isArray(value)) {
330
+ value.forEach((item, index) => {
331
+ appendStripeFormValue(form, `${key}[${index}]`, item);
332
+ });
333
+ return;
334
+ }
335
+ if (typeof value === "object") {
336
+ for (const [childKey, childValue] of Object.entries(value)) appendStripeFormValue(form, `${key}[${childKey}]`, childValue);
337
+ return;
338
+ }
339
+ form.append(key, String(value));
340
+ }
341
+ function mapCustomerToStripe(customer) {
342
+ const stripeCustomer = {
343
+ name: customer.name,
344
+ email: customer.email,
345
+ phone: customer.phone,
346
+ address: customer.billingAddress ? {
347
+ line1: customer.billingAddress.street1,
348
+ line2: customer.billingAddress.street2,
349
+ city: customer.billingAddress.city,
350
+ state: customer.billingAddress.state,
351
+ postal_code: customer.billingAddress.postalCode,
352
+ country: customer.billingAddress.country
353
+ } : void 0,
354
+ metadata: normalizeMetadata({
355
+ local_id: customer.id,
356
+ payment_terms: customer.paymentTerms,
357
+ ...customer.metadata
358
+ })
359
+ };
360
+ if (customer.taxExempt !== void 0) stripeCustomer.tax_exempt = customer.taxExempt ? "exempt" : "none";
361
+ return stripeCustomer;
362
+ }
363
+ function mapInvoiceToStripe(invoice) {
364
+ return {
365
+ customer: invoice.customerExternalId,
366
+ collection_method: "send_invoice",
367
+ due_date: invoice.dueDate,
368
+ auto_advance: false,
369
+ pending_invoice_items_behavior: "include",
370
+ metadata: normalizeMetadata({
371
+ local_id: invoice.id,
372
+ invoice_number: invoice.invoiceNumber,
373
+ reference: invoice.reference,
374
+ memo: invoice.memo,
375
+ tax_amount: invoice.taxAmount,
376
+ subtotal: invoice.subtotal,
377
+ total_amount: invoice.totalAmount,
378
+ ...invoice.metadata
379
+ })
380
+ };
381
+ }
382
+ function mapInvoiceUpdateToStripe(invoice) {
383
+ return {
384
+ due_date: invoice.dueDate,
385
+ metadata: normalizeMetadata({
386
+ local_id: invoice.id,
387
+ invoice_number: invoice.invoiceNumber,
388
+ reference: invoice.reference,
389
+ memo: invoice.memo,
390
+ ...invoice.metadata
391
+ })
392
+ };
393
+ }
394
+ function mapCheckoutSessionToStripe(input) {
395
+ return {
396
+ mode: input.mode || "subscription",
397
+ success_url: input.successUrl,
398
+ cancel_url: input.cancelUrl,
399
+ customer: input.customerExternalId,
400
+ customer_email: input.customerExternalId ? void 0 : input.customerEmail,
401
+ client_reference_id: input.clientReferenceId,
402
+ allow_promotion_codes: input.allowPromotionCodes,
403
+ line_items: input.lineItems.map(mapCheckoutLineItem),
404
+ metadata: normalizeMetadata(input.metadata || {})
405
+ };
406
+ }
407
+ function mapCheckoutLineItem(lineItem) {
408
+ if (lineItem.price) return {
409
+ price: lineItem.price,
410
+ quantity: lineItem.quantity || 1
411
+ };
412
+ if (!lineItem.priceData) throw new Error("Stripe checkout line item requires price or priceData");
413
+ const productData = lineItem.priceData.product ? void 0 : { name: lineItem.priceData.productName || "Subscription" };
414
+ return {
415
+ quantity: lineItem.quantity || 1,
416
+ price_data: {
417
+ currency: lineItem.priceData.currency,
418
+ unit_amount: lineItem.priceData.unitAmount,
419
+ product: lineItem.priceData.product,
420
+ product_data: productData,
421
+ recurring: lineItem.priceData.recurring ? {
422
+ interval: lineItem.priceData.recurring.interval,
423
+ interval_count: lineItem.priceData.recurring.intervalCount
424
+ } : void 0
425
+ }
426
+ };
427
+ }
428
+ function mapListOptions(options) {
429
+ return {
430
+ limit: options.limit,
431
+ starting_after: typeof options.offset === "string" ? options.offset : void 0,
432
+ created: {
433
+ gte: options.startDate,
434
+ lte: options.endDate
435
+ },
436
+ status: options.status
437
+ };
438
+ }
439
+ function mapStripeCustomer(customer) {
440
+ return {
441
+ externalId: customer.id,
442
+ provider: "stripe",
443
+ syncedAt: /* @__PURE__ */ new Date(),
444
+ name: customer.name || "",
445
+ email: customer.email || void 0,
446
+ phone: customer.phone || void 0,
447
+ billingAddress: customer.address ? {
448
+ street1: customer.address.line1 || void 0,
449
+ street2: customer.address.line2 || void 0,
450
+ city: customer.address.city || void 0,
451
+ state: customer.address.state || void 0,
452
+ postalCode: customer.address.postal_code || void 0,
453
+ country: customer.address.country || void 0
454
+ } : void 0,
455
+ balance: customer.balance === null || customer.balance === void 0 ? void 0 : centsToMoney(customer.balance),
456
+ currency: customer.currency || void 0,
457
+ raw: customer
458
+ };
459
+ }
460
+ function mapStripeInvoice(invoice) {
461
+ return {
462
+ externalId: invoice.id,
463
+ provider: "stripe",
464
+ syncedAt: /* @__PURE__ */ new Date(),
465
+ invoiceNumber: invoice.number || invoice.id,
466
+ customerExternalId: typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id || "",
467
+ issueDate: unixToDate(invoice.created) || /* @__PURE__ */ new Date(),
468
+ dueDate: unixToDate(invoice.due_date) || unixToDate(invoice.created) || /* @__PURE__ */ new Date(),
469
+ subtotal: centsToMoney(invoice.subtotal || 0),
470
+ taxAmount: centsToMoney(invoice.tax || 0),
471
+ totalAmount: centsToMoney(invoice.total || 0),
472
+ amountPaid: centsToMoney(invoice.amount_paid || 0),
473
+ balance: centsToMoney(invoice.amount_remaining || 0),
474
+ status: mapStripeInvoiceStatus(invoice.status, invoice.due_date),
475
+ currency: invoice.currency || "usd",
476
+ raw: invoice
477
+ };
478
+ }
479
+ function mapStripePaymentIntent(paymentIntent) {
480
+ return {
481
+ externalId: paymentIntent.id,
482
+ provider: "stripe",
483
+ syncedAt: /* @__PURE__ */ new Date(),
484
+ amount: centsToMoney(paymentIntent.amount || 0),
485
+ currency: paymentIntent.currency || "usd",
486
+ paidAt: unixToDate(paymentIntent.created) || /* @__PURE__ */ new Date(),
487
+ method: paymentIntent.payment_method_types?.[0],
488
+ transactionId: paymentIntent.latest_charge || paymentIntent.id,
489
+ invoiceExternalIds: paymentIntent.invoice ? [paymentIntent.invoice] : void 0,
490
+ status: mapStripePaymentStatus(paymentIntent.status),
491
+ raw: paymentIntent
492
+ };
493
+ }
494
+ function mapStripeSubscription(subscription) {
495
+ return {
496
+ externalId: subscription.id,
497
+ status: subscription.status,
498
+ customerExternalId: typeof subscription.customer === "string" ? subscription.customer : subscription.customer.id,
499
+ currentPeriodStart: unixToDate(subscription.current_period_start),
500
+ currentPeriodEnd: unixToDate(subscription.current_period_end),
501
+ cancelAtPeriodEnd: Boolean(subscription.cancel_at_period_end),
502
+ canceledAt: unixToDate(subscription.canceled_at),
503
+ trialEnd: unixToDate(subscription.trial_end),
504
+ raw: subscription
505
+ };
506
+ }
507
+ function mapStripeInvoiceStatus(status, dueDate) {
508
+ switch (status) {
509
+ case "draft": return "draft";
510
+ case "paid": return "paid";
511
+ case "void": return "voided";
512
+ case "open":
513
+ if (dueDate && dueDate * 1e3 < Date.now()) return "overdue";
514
+ return "sent";
515
+ default: return "sent";
516
+ }
517
+ }
518
+ function mapStripePaymentStatus(status) {
519
+ switch (status) {
520
+ case "succeeded": return "completed";
521
+ case "processing":
522
+ case "requires_action":
523
+ case "requires_capture":
524
+ case "requires_confirmation": return "pending";
525
+ case "canceled":
526
+ case "requires_payment_method": return "failed";
527
+ default: return "pending";
528
+ }
529
+ }
530
+ function mapStripeResourceType(object, eventType) {
531
+ if (object === "customer" || eventType?.startsWith("customer.")) return "customer";
532
+ if (object === "invoice" || eventType?.startsWith("invoice.")) return "invoice";
533
+ if (object === "payment_intent" || object === "charge" || eventType?.startsWith("payment_intent.") || eventType?.startsWith("charge.")) return "payment";
534
+ }
535
+ function normalizeMetadata(metadata) {
536
+ const normalized = {};
537
+ for (const [key, value] of Object.entries(metadata)) if (value !== void 0 && value !== null) normalized[key] = String(value);
538
+ return normalized;
539
+ }
540
+ function parseStripeSignatureHeader(signature) {
541
+ const parts = signature.split(",").map((part) => part.trim());
542
+ const parsed = { signatures: [] };
543
+ for (const part of parts) {
544
+ const separatorIndex = part.indexOf("=");
545
+ if (separatorIndex === -1) continue;
546
+ const key = part.slice(0, separatorIndex);
547
+ const value = part.slice(separatorIndex + 1);
548
+ if (key === "t") parsed.timestamp = value;
549
+ if (key === "v1" && value) parsed.signatures.push(value);
550
+ }
551
+ return parsed;
552
+ }
553
+ function constantTimeEqualHex(left, right) {
554
+ try {
555
+ const leftBuffer = Buffer.from(left, "hex");
556
+ const rightBuffer = Buffer.from(right, "hex");
557
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
558
+ } catch {
559
+ return false;
560
+ }
561
+ }
562
+ function reconcileRecords(provider, locals, externals, compare) {
563
+ const externalById = new Map(externals.map((external) => [external.externalId, external]));
564
+ const matched = [];
565
+ const discrepancies = [];
566
+ const localOnly = [];
567
+ const seenExternalIds = /* @__PURE__ */ new Set();
568
+ for (const local of locals) {
569
+ const external = local.externalId ? externalById.get(local.externalId) : void 0;
570
+ if (!external) {
571
+ localOnly.push(local);
572
+ continue;
573
+ }
574
+ seenExternalIds.add(external.externalId);
575
+ const differences = compare(local, external);
576
+ if (differences.length === 0) matched.push({
577
+ local,
578
+ external,
579
+ status: "identical"
580
+ });
581
+ else discrepancies.push({
582
+ local,
583
+ external,
584
+ differences
585
+ });
586
+ }
587
+ const externalOnly = externals.filter((external) => !seenExternalIds.has(external.externalId));
588
+ return {
589
+ provider,
590
+ auditedAt: /* @__PURE__ */ new Date(),
591
+ matched,
592
+ localOnly,
593
+ externalOnly,
594
+ discrepancies,
595
+ summary: {
596
+ total: locals.length + externalOnly.length,
597
+ matched: matched.length,
598
+ localOnly: localOnly.length,
599
+ externalOnly: externalOnly.length,
600
+ discrepancies: discrepancies.length
601
+ }
602
+ };
603
+ }
604
+ function compareCustomer(local, external) {
605
+ const customer = external;
606
+ return [compareField("name", local.name, customer.name), compareField("email", local.email, customer.email)].filter((diff) => Boolean(diff));
607
+ }
608
+ function compareInvoice(local, external) {
609
+ const invoice = external;
610
+ return [compareField("totalAmount", local.totalAmount, invoice.totalAmount), compareField("currency", local.currency || "usd", invoice.currency)].filter((diff) => Boolean(diff));
611
+ }
612
+ function comparePayment(local, external) {
613
+ const payment = external;
614
+ return [compareField("amount", local.amount, payment.amount), compareField("currency", local.currency || "usd", payment.currency)].filter((diff) => Boolean(diff));
615
+ }
616
+ function compareField(field, localValue, externalValue) {
617
+ if (valuesEqual(localValue, externalValue)) return null;
618
+ return {
619
+ field,
620
+ localValue,
621
+ externalValue
622
+ };
623
+ }
624
+ function valuesEqual(left, right) {
625
+ if (left === right) return true;
626
+ if (typeof left === "number" && typeof right === "number") return Math.abs(left - right) < .01;
627
+ return false;
628
+ }
629
+ function centsToMoney(cents) {
630
+ return cents / 100;
631
+ }
632
+ function toUnixSeconds(date) {
633
+ return Math.floor(date.getTime() / 1e3);
634
+ }
635
+ function unixToDate(value) {
636
+ return value === null || value === void 0 ? void 0 : /* @__PURE__ */ new Date(value * 1e3);
637
+ }
638
+ function sleep(ms) {
639
+ return new Promise((resolve) => setTimeout(resolve, ms));
640
+ }
641
+ //#endregion
642
+ export { StripeProvider };
643
+
644
+ //# sourceMappingURL=stripe-BhTiF2Xx.js.map