stripe-experiment-sync 0.0.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.
Files changed (47) hide show
  1. package/README.md +100 -0
  2. package/dist/index.cjs +2370 -0
  3. package/dist/index.d.cts +222 -0
  4. package/dist/index.d.ts +222 -0
  5. package/dist/index.js +2328 -0
  6. package/dist/migrations/0000_initial_migration.sql +1 -0
  7. package/dist/migrations/0001_products.sql +17 -0
  8. package/dist/migrations/0002_customers.sql +23 -0
  9. package/dist/migrations/0003_prices.sql +34 -0
  10. package/dist/migrations/0004_subscriptions.sql +56 -0
  11. package/dist/migrations/0005_invoices.sql +77 -0
  12. package/dist/migrations/0006_charges.sql +43 -0
  13. package/dist/migrations/0007_coupons.sql +19 -0
  14. package/dist/migrations/0008_disputes.sql +17 -0
  15. package/dist/migrations/0009_events.sql +12 -0
  16. package/dist/migrations/0010_payouts.sql +30 -0
  17. package/dist/migrations/0011_plans.sql +25 -0
  18. package/dist/migrations/0012_add_updated_at.sql +108 -0
  19. package/dist/migrations/0013_add_subscription_items.sql +12 -0
  20. package/dist/migrations/0014_migrate_subscription_items.sql +26 -0
  21. package/dist/migrations/0015_add_customer_deleted.sql +2 -0
  22. package/dist/migrations/0016_add_invoice_indexes.sql +2 -0
  23. package/dist/migrations/0017_drop_charges_unavailable_columns.sql +6 -0
  24. package/dist/migrations/0018_setup_intents.sql +17 -0
  25. package/dist/migrations/0019_payment_methods.sql +12 -0
  26. package/dist/migrations/0020_disputes_payment_intent_created_idx.sql +3 -0
  27. package/dist/migrations/0021_payment_intent.sql +42 -0
  28. package/dist/migrations/0022_adjust_plans.sql +5 -0
  29. package/dist/migrations/0023_invoice_deleted.sql +1 -0
  30. package/dist/migrations/0024_subscription_schedules.sql +29 -0
  31. package/dist/migrations/0025_tax_ids.sql +14 -0
  32. package/dist/migrations/0026_credit_notes.sql +36 -0
  33. package/dist/migrations/0027_add_marketing_features_to_products.sql +2 -0
  34. package/dist/migrations/0028_early_fraud_warning.sql +22 -0
  35. package/dist/migrations/0029_reviews.sql +28 -0
  36. package/dist/migrations/0030_refunds.sql +29 -0
  37. package/dist/migrations/0031_add_default_price.sql +2 -0
  38. package/dist/migrations/0032_update_subscription_items.sql +3 -0
  39. package/dist/migrations/0033_add_last_synced_at.sql +85 -0
  40. package/dist/migrations/0034_remove_foreign_keys.sql +13 -0
  41. package/dist/migrations/0035_checkout_sessions.sql +77 -0
  42. package/dist/migrations/0036_checkout_session_line_items.sql +24 -0
  43. package/dist/migrations/0037_add_features.sql +18 -0
  44. package/dist/migrations/0038_active_entitlement.sql +20 -0
  45. package/dist/migrations/0039_add_paused_to_subscription_status.sql +1 -0
  46. package/dist/migrations/0040_managed_webhooks.sql +28 -0
  47. package/package.json +60 -0
package/dist/index.js ADDED
@@ -0,0 +1,2328 @@
1
+ // src/stripeSync.ts
2
+ import Stripe from "stripe";
3
+ import { pg as sql2 } from "yesql";
4
+
5
+ // src/database/postgres.ts
6
+ import pg from "pg";
7
+ import { pg as sql } from "yesql";
8
+ var PostgresClient = class {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.pool = new pg.Pool(config.poolConfig);
12
+ }
13
+ pool;
14
+ async delete(table, id) {
15
+ const prepared = sql(`
16
+ delete from "${this.config.schema}"."${table}"
17
+ where id = :id
18
+ returning id;
19
+ `)({ id });
20
+ const { rows } = await this.query(prepared.text, prepared.values);
21
+ return rows.length > 0;
22
+ }
23
+ async query(text, params) {
24
+ return this.pool.query(text, params);
25
+ }
26
+ async upsertMany(entries, table, tableSchema) {
27
+ if (!entries.length) return [];
28
+ const chunkSize = 5;
29
+ const results = [];
30
+ for (let i = 0; i < entries.length; i += chunkSize) {
31
+ const chunk = entries.slice(i, i + chunkSize);
32
+ const queries = [];
33
+ chunk.forEach((entry) => {
34
+ const cleansed = this.cleanseArrayField(entry);
35
+ const upsertSql = this.constructUpsertSql(this.config.schema, table, tableSchema);
36
+ const prepared = sql(upsertSql, {
37
+ useNullForMissing: true
38
+ })(cleansed);
39
+ queries.push(this.pool.query(prepared.text, prepared.values));
40
+ });
41
+ results.push(...await Promise.all(queries));
42
+ }
43
+ return results.flatMap((it) => it.rows);
44
+ }
45
+ async upsertManyWithTimestampProtection(entries, table, tableSchema, syncTimestamp) {
46
+ const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
47
+ if (!entries.length) return [];
48
+ const chunkSize = 5;
49
+ const results = [];
50
+ for (let i = 0; i < entries.length; i += chunkSize) {
51
+ const chunk = entries.slice(i, i + chunkSize);
52
+ const queries = [];
53
+ chunk.forEach((entry) => {
54
+ const cleansed = this.cleanseArrayField(entry);
55
+ cleansed.last_synced_at = timestamp;
56
+ const upsertSql = this.constructUpsertWithTimestampProtectionSql(
57
+ this.config.schema,
58
+ table,
59
+ tableSchema
60
+ );
61
+ const prepared = sql(upsertSql, {
62
+ useNullForMissing: true
63
+ })(cleansed);
64
+ queries.push(this.pool.query(prepared.text, prepared.values));
65
+ });
66
+ results.push(...await Promise.all(queries));
67
+ }
68
+ return results.flatMap((it) => it.rows);
69
+ }
70
+ async findMissingEntries(table, ids) {
71
+ if (!ids.length) return [];
72
+ const prepared = sql(`
73
+ select id from "${this.config.schema}"."${table}"
74
+ where id=any(:ids::text[]);
75
+ `)({ ids });
76
+ const { rows } = await this.query(prepared.text, prepared.values);
77
+ const existingIds = rows.map((it) => it.id);
78
+ const missingIds = ids.filter((it) => !existingIds.includes(it));
79
+ return missingIds;
80
+ }
81
+ /**
82
+ * Returns an (yesql formatted) upsert function based on the key/vals of an object.
83
+ * eg,
84
+ * insert into customers ("id", "name")
85
+ * values (:id, :name)
86
+ * on conflict (id)
87
+ * do update set (
88
+ * "id" = :id,
89
+ * "name" = :name
90
+ * )
91
+ */
92
+ constructUpsertSql(schema, table, tableSchema, options) {
93
+ const { conflict = "id" } = options || {};
94
+ const properties = tableSchema.properties;
95
+ return `
96
+ insert into "${schema}"."${table}" (
97
+ ${properties.map((x) => `"${x}"`).join(",")}
98
+ )
99
+ values (
100
+ ${properties.map((x) => `:${x}`).join(",")}
101
+ )
102
+ on conflict (
103
+ ${conflict}
104
+ )
105
+ do update set
106
+ ${properties.map((x) => `"${x}" = :${x}`).join(",")}
107
+ ;`;
108
+ }
109
+ /**
110
+ * Returns an (yesql formatted) upsert function with timestamp protection.
111
+ *
112
+ * The WHERE clause in ON CONFLICT DO UPDATE only applies to the conflicting row
113
+ * (the row being updated), not to all rows in the table. PostgreSQL ensures that
114
+ * the condition is evaluated only for the specific row that conflicts with the INSERT.
115
+ *
116
+ *
117
+ * eg:
118
+ * INSERT INTO "stripe"."charges" (
119
+ * "id", "amount", "created", "last_synced_at"
120
+ * )
121
+ * VALUES (
122
+ * :id, :amount, :created, :last_synced_at
123
+ * )
124
+ * ON CONFLICT (id) DO UPDATE SET
125
+ * "amount" = EXCLUDED."amount",
126
+ * "created" = EXCLUDED."created",
127
+ * last_synced_at = :last_synced_at
128
+ * WHERE "charges"."last_synced_at" IS NULL
129
+ * OR "charges"."last_synced_at" < :last_synced_at;
130
+ */
131
+ constructUpsertWithTimestampProtectionSql = (schema, table, tableSchema) => {
132
+ const conflict = "id";
133
+ const properties = tableSchema.properties;
134
+ return `
135
+ INSERT INTO "${schema}"."${table}" (
136
+ ${properties.map((x) => `"${x}"`).join(",")}, "last_synced_at"
137
+ )
138
+ VALUES (
139
+ ${properties.map((x) => `:${x}`).join(",")}, :last_synced_at
140
+ )
141
+ ON CONFLICT (${conflict}) DO UPDATE SET
142
+ ${properties.filter((x) => x !== "last_synced_at").map((x) => `"${x}" = EXCLUDED."${x}"`).join(",")},
143
+ last_synced_at = :last_synced_at
144
+ WHERE "${table}"."last_synced_at" IS NULL
145
+ OR "${table}"."last_synced_at" < :last_synced_at;`;
146
+ };
147
+ /**
148
+ * For array object field like invoice.custom_fields
149
+ * ex: [{"name":"Project name","value":"Test Project"}]
150
+ *
151
+ * we need to stringify it first cos passing array object directly will end up with
152
+ * {
153
+ * invalid input syntax for type json
154
+ * detail: 'Expected ":", but found "}".',
155
+ * where: 'JSON data, line 1: ...\\":\\"Project name\\",\\"value\\":\\"Test Project\\"}"}',
156
+ * }
157
+ */
158
+ cleanseArrayField(obj) {
159
+ const cleansed = { ...obj };
160
+ Object.keys(cleansed).map((k) => {
161
+ const data = cleansed[k];
162
+ if (Array.isArray(data)) {
163
+ cleansed[k] = JSON.stringify(data);
164
+ }
165
+ });
166
+ return cleansed;
167
+ }
168
+ };
169
+
170
+ // src/schemas/charge.ts
171
+ var chargeSchema = {
172
+ properties: [
173
+ "id",
174
+ "object",
175
+ "paid",
176
+ "order",
177
+ "amount",
178
+ "review",
179
+ "source",
180
+ "status",
181
+ "created",
182
+ "dispute",
183
+ "invoice",
184
+ "outcome",
185
+ "refunds",
186
+ "captured",
187
+ "currency",
188
+ "customer",
189
+ "livemode",
190
+ "metadata",
191
+ "refunded",
192
+ "shipping",
193
+ "application",
194
+ "description",
195
+ "destination",
196
+ "failure_code",
197
+ "on_behalf_of",
198
+ "fraud_details",
199
+ "receipt_email",
200
+ "payment_intent",
201
+ "receipt_number",
202
+ "transfer_group",
203
+ "amount_refunded",
204
+ "application_fee",
205
+ "failure_message",
206
+ "source_transfer",
207
+ "balance_transaction",
208
+ "statement_descriptor",
209
+ "payment_method_details"
210
+ ]
211
+ };
212
+
213
+ // src/schemas/checkout_sessions.ts
214
+ var checkoutSessionSchema = {
215
+ properties: [
216
+ "id",
217
+ "object",
218
+ "adaptive_pricing",
219
+ "after_expiration",
220
+ "allow_promotion_codes",
221
+ "amount_subtotal",
222
+ "amount_total",
223
+ "automatic_tax",
224
+ "billing_address_collection",
225
+ "cancel_url",
226
+ "client_reference_id",
227
+ "client_secret",
228
+ "collected_information",
229
+ "consent",
230
+ "consent_collection",
231
+ "created",
232
+ "currency",
233
+ "currency_conversion",
234
+ "custom_fields",
235
+ "custom_text",
236
+ "customer",
237
+ "customer_creation",
238
+ "customer_details",
239
+ "customer_email",
240
+ "discounts",
241
+ "expires_at",
242
+ "invoice",
243
+ "invoice_creation",
244
+ "livemode",
245
+ "locale",
246
+ "metadata",
247
+ "mode",
248
+ "optional_items",
249
+ "payment_intent",
250
+ "payment_link",
251
+ "payment_method_collection",
252
+ "payment_method_configuration_details",
253
+ "payment_method_options",
254
+ "payment_method_types",
255
+ "payment_status",
256
+ "permissions",
257
+ "phone_number_collection",
258
+ "presentment_details",
259
+ "recovered_from",
260
+ "redirect_on_completion",
261
+ "return_url",
262
+ "saved_payment_method_options",
263
+ "setup_intent",
264
+ "shipping_address_collection",
265
+ "shipping_cost",
266
+ "shipping_details",
267
+ "shipping_options",
268
+ "status",
269
+ "submit_type",
270
+ "subscription",
271
+ "success_url",
272
+ "tax_id_collection",
273
+ "total_details",
274
+ "ui_mode",
275
+ "url",
276
+ "wallet_options"
277
+ ]
278
+ };
279
+
280
+ // src/schemas/checkout_session_line_items.ts
281
+ var checkoutSessionLineItemSchema = {
282
+ properties: [
283
+ "id",
284
+ "object",
285
+ "amount_discount",
286
+ "amount_subtotal",
287
+ "amount_tax",
288
+ "amount_total",
289
+ "currency",
290
+ "description",
291
+ "price",
292
+ "quantity",
293
+ "checkout_session"
294
+ ]
295
+ };
296
+
297
+ // src/schemas/credit_note.ts
298
+ var creditNoteSchema = {
299
+ properties: [
300
+ "id",
301
+ "object",
302
+ "amount",
303
+ "amount_shipping",
304
+ "created",
305
+ "currency",
306
+ "customer",
307
+ "customer_balance_transaction",
308
+ "discount_amount",
309
+ "discount_amounts",
310
+ "invoice",
311
+ "lines",
312
+ "livemode",
313
+ "memo",
314
+ "metadata",
315
+ "number",
316
+ "out_of_band_amount",
317
+ "pdf",
318
+ "reason",
319
+ "refund",
320
+ "shipping_cost",
321
+ "status",
322
+ "subtotal",
323
+ "subtotal_excluding_tax",
324
+ "tax_amounts",
325
+ "total",
326
+ "total_excluding_tax",
327
+ "type",
328
+ "voided_at"
329
+ ]
330
+ };
331
+
332
+ // src/schemas/customer.ts
333
+ var customerSchema = {
334
+ properties: [
335
+ "id",
336
+ "object",
337
+ "address",
338
+ "description",
339
+ "email",
340
+ "metadata",
341
+ "name",
342
+ "phone",
343
+ "shipping",
344
+ "balance",
345
+ "created",
346
+ "currency",
347
+ "default_source",
348
+ "delinquent",
349
+ "discount",
350
+ "invoice_prefix",
351
+ "invoice_settings",
352
+ "livemode",
353
+ "next_invoice_sequence",
354
+ "preferred_locales",
355
+ "tax_exempt"
356
+ ]
357
+ };
358
+ var customerDeletedSchema = {
359
+ properties: ["id", "object", "deleted"]
360
+ };
361
+
362
+ // src/schemas/dispute.ts
363
+ var disputeSchema = {
364
+ properties: [
365
+ "id",
366
+ "object",
367
+ "amount",
368
+ "charge",
369
+ "created",
370
+ "currency",
371
+ "balance_transactions",
372
+ "evidence",
373
+ "evidence_details",
374
+ "is_charge_refundable",
375
+ "livemode",
376
+ "metadata",
377
+ "payment_intent",
378
+ "reason",
379
+ "status"
380
+ ]
381
+ };
382
+
383
+ // src/schemas/invoice.ts
384
+ var invoiceSchema = {
385
+ properties: [
386
+ "id",
387
+ "object",
388
+ "auto_advance",
389
+ "collection_method",
390
+ "currency",
391
+ "description",
392
+ "hosted_invoice_url",
393
+ "lines",
394
+ "metadata",
395
+ "period_end",
396
+ "period_start",
397
+ "status",
398
+ "total",
399
+ "account_country",
400
+ "account_name",
401
+ "account_tax_ids",
402
+ "amount_due",
403
+ "amount_paid",
404
+ "amount_remaining",
405
+ "application_fee_amount",
406
+ "attempt_count",
407
+ "attempted",
408
+ "billing_reason",
409
+ "created",
410
+ "custom_fields",
411
+ "customer_address",
412
+ "customer_email",
413
+ "customer_name",
414
+ "customer_phone",
415
+ "customer_shipping",
416
+ "customer_tax_exempt",
417
+ "customer_tax_ids",
418
+ "default_tax_rates",
419
+ "discount",
420
+ "discounts",
421
+ "due_date",
422
+ "ending_balance",
423
+ "footer",
424
+ "invoice_pdf",
425
+ "last_finalization_error",
426
+ "livemode",
427
+ "next_payment_attempt",
428
+ "number",
429
+ "paid",
430
+ "payment_settings",
431
+ "post_payment_credit_notes_amount",
432
+ "pre_payment_credit_notes_amount",
433
+ "receipt_number",
434
+ "starting_balance",
435
+ "statement_descriptor",
436
+ "status_transitions",
437
+ "subtotal",
438
+ "tax",
439
+ "total_discount_amounts",
440
+ "total_tax_amounts",
441
+ "transfer_data",
442
+ "webhooks_delivered_at",
443
+ "customer",
444
+ "subscription",
445
+ "payment_intent",
446
+ "default_payment_method",
447
+ "default_source",
448
+ "on_behalf_of",
449
+ "charge"
450
+ ]
451
+ };
452
+
453
+ // src/schemas/plan.ts
454
+ var planSchema = {
455
+ properties: [
456
+ "id",
457
+ "object",
458
+ "active",
459
+ "amount",
460
+ "created",
461
+ "product",
462
+ "currency",
463
+ "interval",
464
+ "livemode",
465
+ "metadata",
466
+ "nickname",
467
+ "tiers_mode",
468
+ "usage_type",
469
+ "billing_scheme",
470
+ "interval_count",
471
+ "aggregate_usage",
472
+ "transform_usage",
473
+ "trial_period_days"
474
+ ]
475
+ };
476
+
477
+ // src/schemas/price.ts
478
+ var priceSchema = {
479
+ properties: [
480
+ "id",
481
+ "object",
482
+ "active",
483
+ "currency",
484
+ "metadata",
485
+ "nickname",
486
+ "recurring",
487
+ "type",
488
+ "unit_amount",
489
+ "billing_scheme",
490
+ "created",
491
+ "livemode",
492
+ "lookup_key",
493
+ "tiers_mode",
494
+ "transform_quantity",
495
+ "unit_amount_decimal",
496
+ "product"
497
+ ]
498
+ };
499
+
500
+ // src/schemas/product.ts
501
+ var productSchema = {
502
+ properties: [
503
+ "id",
504
+ "object",
505
+ "active",
506
+ "default_price",
507
+ "description",
508
+ "metadata",
509
+ "name",
510
+ "created",
511
+ "images",
512
+ "marketing_features",
513
+ "livemode",
514
+ "package_dimensions",
515
+ "shippable",
516
+ "statement_descriptor",
517
+ "unit_label",
518
+ "updated",
519
+ "url"
520
+ ]
521
+ };
522
+
523
+ // src/schemas/payment_intent.ts
524
+ var paymentIntentSchema = {
525
+ properties: [
526
+ "id",
527
+ "object",
528
+ "amount",
529
+ "amount_capturable",
530
+ "amount_details",
531
+ "amount_received",
532
+ "application",
533
+ "application_fee_amount",
534
+ "automatic_payment_methods",
535
+ "canceled_at",
536
+ "cancellation_reason",
537
+ "capture_method",
538
+ "client_secret",
539
+ "confirmation_method",
540
+ "created",
541
+ "currency",
542
+ "customer",
543
+ "description",
544
+ "invoice",
545
+ "last_payment_error",
546
+ "livemode",
547
+ "metadata",
548
+ "next_action",
549
+ "on_behalf_of",
550
+ "payment_method",
551
+ "payment_method_options",
552
+ "payment_method_types",
553
+ "processing",
554
+ "receipt_email",
555
+ "review",
556
+ "setup_future_usage",
557
+ "shipping",
558
+ "statement_descriptor",
559
+ "statement_descriptor_suffix",
560
+ "status",
561
+ "transfer_data",
562
+ "transfer_group"
563
+ ]
564
+ };
565
+
566
+ // src/schemas/payment_methods.ts
567
+ var paymentMethodsSchema = {
568
+ properties: [
569
+ "id",
570
+ "object",
571
+ "created",
572
+ "customer",
573
+ "type",
574
+ "billing_details",
575
+ "metadata",
576
+ "card"
577
+ ]
578
+ };
579
+
580
+ // src/schemas/setup_intents.ts
581
+ var setupIntentsSchema = {
582
+ properties: [
583
+ "id",
584
+ "object",
585
+ "created",
586
+ "customer",
587
+ "description",
588
+ "payment_method",
589
+ "status",
590
+ "usage",
591
+ "cancellation_reason",
592
+ "latest_attempt",
593
+ "mandate",
594
+ "single_use_mandate",
595
+ "on_behalf_of"
596
+ ]
597
+ };
598
+
599
+ // src/schemas/tax_id.ts
600
+ var taxIdSchema = {
601
+ properties: [
602
+ "id",
603
+ "country",
604
+ "customer",
605
+ "type",
606
+ "value",
607
+ "object",
608
+ "created",
609
+ "livemode",
610
+ "owner"
611
+ ]
612
+ };
613
+
614
+ // src/schemas/subscription_item.ts
615
+ var subscriptionItemSchema = {
616
+ properties: [
617
+ "id",
618
+ "object",
619
+ "billing_thresholds",
620
+ "created",
621
+ "deleted",
622
+ "metadata",
623
+ "quantity",
624
+ "price",
625
+ "subscription",
626
+ "tax_rates",
627
+ "current_period_end",
628
+ "current_period_start"
629
+ ]
630
+ };
631
+
632
+ // src/schemas/subscription_schedules.ts
633
+ var subscriptionScheduleSchema = {
634
+ properties: [
635
+ "id",
636
+ "object",
637
+ "application",
638
+ "canceled_at",
639
+ "completed_at",
640
+ "created",
641
+ "current_phase",
642
+ "customer",
643
+ "default_settings",
644
+ "end_behavior",
645
+ "livemode",
646
+ "metadata",
647
+ "phases",
648
+ "released_at",
649
+ "released_subscription",
650
+ "status",
651
+ "subscription",
652
+ "test_clock"
653
+ ]
654
+ };
655
+
656
+ // src/schemas/subscription.ts
657
+ var subscriptionSchema = {
658
+ properties: [
659
+ "id",
660
+ "object",
661
+ "cancel_at_period_end",
662
+ "current_period_end",
663
+ "current_period_start",
664
+ "default_payment_method",
665
+ "items",
666
+ "metadata",
667
+ "pending_setup_intent",
668
+ "pending_update",
669
+ "status",
670
+ "application_fee_percent",
671
+ "billing_cycle_anchor",
672
+ "billing_thresholds",
673
+ "cancel_at",
674
+ "canceled_at",
675
+ "collection_method",
676
+ "created",
677
+ "days_until_due",
678
+ "default_source",
679
+ "default_tax_rates",
680
+ "discount",
681
+ "ended_at",
682
+ "livemode",
683
+ "next_pending_invoice_item_invoice",
684
+ "pause_collection",
685
+ "pending_invoice_item_interval",
686
+ "start_date",
687
+ "transfer_data",
688
+ "trial_end",
689
+ "trial_start",
690
+ "schedule",
691
+ "customer",
692
+ "latest_invoice",
693
+ "plan"
694
+ ]
695
+ };
696
+
697
+ // src/schemas/early_fraud_warning.ts
698
+ var earlyFraudWarningSchema = {
699
+ properties: [
700
+ "id",
701
+ "object",
702
+ "actionable",
703
+ "charge",
704
+ "created",
705
+ "fraud_type",
706
+ "livemode",
707
+ "payment_intent"
708
+ ]
709
+ };
710
+
711
+ // src/schemas/review.ts
712
+ var reviewSchema = {
713
+ properties: [
714
+ "id",
715
+ "object",
716
+ "billing_zip",
717
+ "created",
718
+ "charge",
719
+ "closed_reason",
720
+ "livemode",
721
+ "ip_address",
722
+ "ip_address_location",
723
+ "open",
724
+ "opened_reason",
725
+ "payment_intent",
726
+ "reason",
727
+ "session"
728
+ ]
729
+ };
730
+
731
+ // src/schemas/refund.ts
732
+ var refundSchema = {
733
+ properties: [
734
+ "id",
735
+ "object",
736
+ "amount",
737
+ "balance_transaction",
738
+ "charge",
739
+ "created",
740
+ "currency",
741
+ "destination_details",
742
+ "metadata",
743
+ "payment_intent",
744
+ "reason",
745
+ "receipt_number",
746
+ "source_transfer_reversal",
747
+ "status",
748
+ "transfer_reversal"
749
+ ]
750
+ };
751
+
752
+ // src/schemas/active_entitlement.ts
753
+ var activeEntitlementSchema = {
754
+ properties: ["id", "object", "feature", "lookup_key", "livemode", "customer"]
755
+ };
756
+
757
+ // src/schemas/feature.ts
758
+ var featureSchema = {
759
+ properties: ["id", "object", "livemode", "name", "lookup_key", "active", "metadata"]
760
+ };
761
+
762
+ // src/schemas/managed_webhook.ts
763
+ var managedWebhookSchema = {
764
+ properties: [
765
+ "id",
766
+ "object",
767
+ "uuid",
768
+ "url",
769
+ "enabled_events",
770
+ "description",
771
+ "enabled",
772
+ "livemode",
773
+ "metadata",
774
+ "secret",
775
+ "status",
776
+ "api_version",
777
+ "created"
778
+ ]
779
+ };
780
+
781
+ // src/stripeSync.ts
782
+ import { randomUUID } from "crypto";
783
+
784
+ // src/database/migrate.ts
785
+ import { Client } from "pg";
786
+ import { migrate } from "pg-node-migrations";
787
+ import fs from "fs";
788
+ import path from "path";
789
+ import { fileURLToPath } from "url";
790
+ var __filename2 = fileURLToPath(import.meta.url);
791
+ var __dirname2 = path.dirname(__filename2);
792
+ async function connectAndMigrate(client, migrationsDirectory, config, logOnError = false) {
793
+ if (!fs.existsSync(migrationsDirectory)) {
794
+ config.logger?.info(`Migrations directory ${migrationsDirectory} not found, skipping`);
795
+ return;
796
+ }
797
+ const optionalConfig = {
798
+ schemaName: config.schema,
799
+ tableName: "migrations"
800
+ };
801
+ try {
802
+ await migrate({ client }, migrationsDirectory, optionalConfig);
803
+ } catch (error) {
804
+ if (logOnError && error instanceof Error) {
805
+ config.logger?.error(error, "Migration error:");
806
+ } else {
807
+ throw error;
808
+ }
809
+ }
810
+ }
811
+ async function runMigrations(config) {
812
+ const client = new Client({
813
+ connectionString: config.databaseUrl,
814
+ ssl: config.ssl,
815
+ connectionTimeoutMillis: 1e4
816
+ });
817
+ try {
818
+ await client.connect();
819
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${config.schema};`);
820
+ config.logger?.info("Running migrations");
821
+ await connectAndMigrate(client, path.resolve(__dirname2, "./migrations"), config);
822
+ } catch (err) {
823
+ config.logger?.error(err, "Error running migrations");
824
+ throw err;
825
+ } finally {
826
+ await client.end();
827
+ config.logger?.info("Finished migrations");
828
+ }
829
+ }
830
+
831
+ // src/stripeSync.ts
832
+ import express from "express";
833
+ function getUniqueIds(entries, key) {
834
+ const set = new Set(
835
+ entries.map((subscription) => subscription?.[key]?.toString()).filter((it) => Boolean(it))
836
+ );
837
+ return Array.from(set);
838
+ }
839
+ var DEFAULT_SCHEMA = "stripe";
840
+ var StripeAutoSync = class {
841
+ options;
842
+ webhookId = null;
843
+ webhookUuid = null;
844
+ stripeSync = null;
845
+ constructor(options) {
846
+ this.options = {
847
+ ...options,
848
+ // Apply defaults for undefined values
849
+ schema: options.schema || "stripe",
850
+ webhookPath: options.webhookPath || "/stripe-webhooks",
851
+ stripeApiVersion: options.stripeApiVersion || "2020-08-27",
852
+ autoExpandLists: options.autoExpandLists !== void 0 ? options.autoExpandLists : false,
853
+ backfillRelatedEntities: options.backfillRelatedEntities !== void 0 ? options.backfillRelatedEntities : true
854
+ };
855
+ }
856
+ /**
857
+ * Starts the Stripe Sync infrastructure and mounts webhook handler:
858
+ * 1. Runs database migrations
859
+ * 2. Creates StripeSync instance
860
+ * 3. Creates managed webhook endpoint
861
+ * 4. Mounts webhook handler on provided Express app
862
+ * 5. Applies body parsing middleware (automatically skips webhook routes)
863
+ *
864
+ * @param app - Express app to mount webhook handler on
865
+ * @returns Information about the running instance
866
+ */
867
+ async start(app) {
868
+ try {
869
+ await runMigrations({
870
+ databaseUrl: this.options.databaseUrl,
871
+ schema: this.options.schema
872
+ });
873
+ const poolConfig = {
874
+ max: 10,
875
+ connectionString: this.options.databaseUrl,
876
+ keepAlive: true
877
+ };
878
+ this.stripeSync = new StripeSync({
879
+ databaseUrl: this.options.databaseUrl,
880
+ schema: this.options.schema,
881
+ stripeSecretKey: this.options.stripeApiKey,
882
+ stripeApiVersion: this.options.stripeApiVersion,
883
+ autoExpandLists: this.options.autoExpandLists,
884
+ backfillRelatedEntities: this.options.backfillRelatedEntities,
885
+ poolConfig
886
+ });
887
+ const baseUrl = this.options.baseUrl();
888
+ const { webhook, uuid } = await this.stripeSync.createManagedWebhook(
889
+ `${baseUrl}${this.options.webhookPath}`,
890
+ {
891
+ enabled_events: ["*"],
892
+ // Subscribe to all events
893
+ description: "stripe-sync-cli development webhook"
894
+ }
895
+ );
896
+ this.webhookId = webhook.id;
897
+ this.webhookUuid = uuid;
898
+ this.mountWebhook(app);
899
+ app.use(this.getBodyParserMiddleware());
900
+ return {
901
+ baseUrl,
902
+ webhookUrl: webhook.url,
903
+ webhookUuid: uuid
904
+ };
905
+ } catch (error) {
906
+ if (error instanceof Error) {
907
+ console.error("Failed to start Stripe Sync:", error.message);
908
+ console.error(error.stack || "");
909
+ } else {
910
+ console.error("Failed to start Stripe Sync:", String(error));
911
+ }
912
+ await this.stop();
913
+ throw error;
914
+ }
915
+ }
916
+ /**
917
+ * Stops all services and cleans up resources:
918
+ * 1. Deletes Stripe webhook endpoint from Stripe and database
919
+ */
920
+ async stop() {
921
+ if (this.webhookId && this.stripeSync) {
922
+ try {
923
+ await this.stripeSync.deleteManagedWebhook(this.webhookId);
924
+ } catch (error) {
925
+ console.error("Could not delete webhook:", error);
926
+ }
927
+ }
928
+ }
929
+ /**
930
+ * Returns Express middleware for body parsing that automatically skips webhook routes.
931
+ * This middleware applies JSON and URL-encoded parsers to all routes EXCEPT the webhook path,
932
+ * which needs raw body for signature verification.
933
+ *
934
+ * @returns Express middleware function
935
+ */
936
+ getBodyParserMiddleware() {
937
+ const webhookPath = this.options.webhookPath;
938
+ return (req, res, next) => {
939
+ if (req.path.startsWith(webhookPath)) {
940
+ return next();
941
+ }
942
+ express.json()(req, res, (err) => {
943
+ if (err) return next(err);
944
+ express.urlencoded({ extended: false })(req, res, next);
945
+ });
946
+ };
947
+ }
948
+ /**
949
+ * Mounts the Stripe webhook handler on the provided Express app.
950
+ * Applies raw body parser middleware for signature verification.
951
+ * IMPORTANT: Must be called BEFORE app.use(express.json()) to ensure raw body parsing.
952
+ */
953
+ mountWebhook(app) {
954
+ const webhookRoute = `${this.options.webhookPath}/:uuid`;
955
+ app.use(webhookRoute, express.raw({ type: "application/json" }));
956
+ app.post(webhookRoute, async (req, res) => {
957
+ const sig = req.headers["stripe-signature"];
958
+ if (!sig || typeof sig !== "string") {
959
+ console.error("[Webhook] Missing stripe-signature header");
960
+ return res.status(400).send({ error: "Missing stripe-signature header" });
961
+ }
962
+ const { uuid } = req.params;
963
+ const rawBody = req.body;
964
+ if (!rawBody || !Buffer.isBuffer(rawBody)) {
965
+ console.error("[Webhook] Body is not a Buffer!", {
966
+ hasBody: !!rawBody,
967
+ bodyType: typeof rawBody,
968
+ isBuffer: Buffer.isBuffer(rawBody),
969
+ bodyConstructor: rawBody?.constructor?.name
970
+ });
971
+ return res.status(400).send({ error: "Missing raw body for signature verification" });
972
+ }
973
+ try {
974
+ await this.stripeSync.processWebhook(rawBody, sig, uuid);
975
+ return res.status(200).send({ received: true });
976
+ } catch (error) {
977
+ console.error("[Webhook] Processing error:", error.message);
978
+ return res.status(400).send({ error: error.message });
979
+ }
980
+ });
981
+ }
982
+ };
983
+ var StripeSync = class {
984
+ constructor(config) {
985
+ this.config = config;
986
+ this.stripe = new Stripe(config.stripeSecretKey, {
987
+ // https://github.com/stripe/stripe-node#configuration
988
+ // @ts-ignore
989
+ apiVersion: config.stripeApiVersion,
990
+ appInfo: {
991
+ name: "Stripe Postgres Sync"
992
+ }
993
+ });
994
+ this.config.logger?.info(
995
+ { autoExpandLists: config.autoExpandLists, stripeApiVersion: config.stripeApiVersion },
996
+ "StripeSync initialized"
997
+ );
998
+ const poolConfig = config.poolConfig ?? {};
999
+ if (config.databaseUrl) {
1000
+ poolConfig.connectionString = config.databaseUrl;
1001
+ }
1002
+ if (config.maxPostgresConnections) {
1003
+ poolConfig.max = config.maxPostgresConnections;
1004
+ }
1005
+ if (poolConfig.max === void 0) {
1006
+ poolConfig.max = 10;
1007
+ }
1008
+ if (poolConfig.keepAlive === void 0) {
1009
+ poolConfig.keepAlive = true;
1010
+ }
1011
+ this.postgresClient = new PostgresClient({
1012
+ schema: config.schema || DEFAULT_SCHEMA,
1013
+ poolConfig
1014
+ });
1015
+ }
1016
+ stripe;
1017
+ postgresClient;
1018
+ async processWebhook(payload, signature, uuid) {
1019
+ const result = await this.postgresClient.query(
1020
+ `SELECT secret FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" WHERE uuid = $1`,
1021
+ [uuid]
1022
+ );
1023
+ if (result.rows.length === 0) {
1024
+ throw new Error(`No managed webhook found with UUID: ${uuid}`);
1025
+ }
1026
+ const webhookSecret = result.rows[0].secret;
1027
+ const event = await this.stripe.webhooks.constructEventAsync(
1028
+ payload,
1029
+ signature,
1030
+ webhookSecret
1031
+ );
1032
+ return this.processEvent(event);
1033
+ }
1034
+ async processEvent(event) {
1035
+ switch (event.type) {
1036
+ case "charge.captured":
1037
+ case "charge.expired":
1038
+ case "charge.failed":
1039
+ case "charge.pending":
1040
+ case "charge.refunded":
1041
+ case "charge.succeeded":
1042
+ case "charge.updated": {
1043
+ const { entity: charge, refetched } = await this.fetchOrUseWebhookData(
1044
+ event.data.object,
1045
+ (id) => this.stripe.charges.retrieve(id),
1046
+ (charge2) => charge2.status === "failed" || charge2.status === "succeeded"
1047
+ );
1048
+ this.config.logger?.info(
1049
+ `Received webhook ${event.id}: ${event.type} for charge ${charge.id}`
1050
+ );
1051
+ await this.upsertCharges([charge], false, this.getSyncTimestamp(event, refetched));
1052
+ break;
1053
+ }
1054
+ case "customer.deleted": {
1055
+ const customer = {
1056
+ id: event.data.object.id,
1057
+ object: "customer",
1058
+ deleted: true
1059
+ };
1060
+ this.config.logger?.info(
1061
+ `Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
1062
+ );
1063
+ await this.upsertCustomers([customer], this.getSyncTimestamp(event, false));
1064
+ break;
1065
+ }
1066
+ case "checkout.session.async_payment_failed":
1067
+ case "checkout.session.async_payment_succeeded":
1068
+ case "checkout.session.completed":
1069
+ case "checkout.session.expired": {
1070
+ const { entity: checkoutSession, refetched } = await this.fetchOrUseWebhookData(
1071
+ event.data.object,
1072
+ (id) => this.stripe.checkout.sessions.retrieve(id)
1073
+ );
1074
+ this.config.logger?.info(
1075
+ `Received webhook ${event.id}: ${event.type} for checkout session ${checkoutSession.id}`
1076
+ );
1077
+ await this.upsertCheckoutSessions(
1078
+ [checkoutSession],
1079
+ false,
1080
+ this.getSyncTimestamp(event, refetched)
1081
+ );
1082
+ break;
1083
+ }
1084
+ case "customer.created":
1085
+ case "customer.updated": {
1086
+ const { entity: customer, refetched } = await this.fetchOrUseWebhookData(
1087
+ event.data.object,
1088
+ (id) => this.stripe.customers.retrieve(id),
1089
+ (customer2) => customer2.deleted === true
1090
+ );
1091
+ this.config.logger?.info(
1092
+ `Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
1093
+ );
1094
+ await this.upsertCustomers([customer], this.getSyncTimestamp(event, refetched));
1095
+ break;
1096
+ }
1097
+ case "customer.subscription.created":
1098
+ case "customer.subscription.deleted":
1099
+ // Soft delete using `status = canceled`
1100
+ case "customer.subscription.paused":
1101
+ case "customer.subscription.pending_update_applied":
1102
+ case "customer.subscription.pending_update_expired":
1103
+ case "customer.subscription.trial_will_end":
1104
+ case "customer.subscription.resumed":
1105
+ case "customer.subscription.updated": {
1106
+ const { entity: subscription, refetched } = await this.fetchOrUseWebhookData(
1107
+ event.data.object,
1108
+ (id) => this.stripe.subscriptions.retrieve(id),
1109
+ (subscription2) => subscription2.status === "canceled" || subscription2.status === "incomplete_expired"
1110
+ );
1111
+ this.config.logger?.info(
1112
+ `Received webhook ${event.id}: ${event.type} for subscription ${subscription.id}`
1113
+ );
1114
+ await this.upsertSubscriptions(
1115
+ [subscription],
1116
+ false,
1117
+ this.getSyncTimestamp(event, refetched)
1118
+ );
1119
+ break;
1120
+ }
1121
+ case "customer.tax_id.updated":
1122
+ case "customer.tax_id.created": {
1123
+ const { entity: taxId, refetched } = await this.fetchOrUseWebhookData(
1124
+ event.data.object,
1125
+ (id) => this.stripe.taxIds.retrieve(id)
1126
+ );
1127
+ this.config.logger?.info(
1128
+ `Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
1129
+ );
1130
+ await this.upsertTaxIds([taxId], false, this.getSyncTimestamp(event, refetched));
1131
+ break;
1132
+ }
1133
+ case "customer.tax_id.deleted": {
1134
+ const taxId = event.data.object;
1135
+ this.config.logger?.info(
1136
+ `Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
1137
+ );
1138
+ await this.deleteTaxId(taxId.id);
1139
+ break;
1140
+ }
1141
+ case "invoice.created":
1142
+ case "invoice.deleted":
1143
+ case "invoice.finalized":
1144
+ case "invoice.finalization_failed":
1145
+ case "invoice.paid":
1146
+ case "invoice.payment_action_required":
1147
+ case "invoice.payment_failed":
1148
+ case "invoice.payment_succeeded":
1149
+ case "invoice.upcoming":
1150
+ case "invoice.sent":
1151
+ case "invoice.voided":
1152
+ case "invoice.marked_uncollectible":
1153
+ case "invoice.updated": {
1154
+ const { entity: invoice, refetched } = await this.fetchOrUseWebhookData(
1155
+ event.data.object,
1156
+ (id) => this.stripe.invoices.retrieve(id),
1157
+ (invoice2) => invoice2.status === "void"
1158
+ );
1159
+ this.config.logger?.info(
1160
+ `Received webhook ${event.id}: ${event.type} for invoice ${invoice.id}`
1161
+ );
1162
+ await this.upsertInvoices([invoice], false, this.getSyncTimestamp(event, refetched));
1163
+ break;
1164
+ }
1165
+ case "product.created":
1166
+ case "product.updated": {
1167
+ try {
1168
+ const { entity: product, refetched } = await this.fetchOrUseWebhookData(
1169
+ event.data.object,
1170
+ (id) => this.stripe.products.retrieve(id)
1171
+ );
1172
+ this.config.logger?.info(
1173
+ `Received webhook ${event.id}: ${event.type} for product ${product.id}`
1174
+ );
1175
+ await this.upsertProducts([product], this.getSyncTimestamp(event, refetched));
1176
+ } catch (err) {
1177
+ if (err instanceof Stripe.errors.StripeAPIError && err.code === "resource_missing") {
1178
+ await this.deleteProduct(event.data.object.id);
1179
+ } else {
1180
+ throw err;
1181
+ }
1182
+ }
1183
+ break;
1184
+ }
1185
+ case "product.deleted": {
1186
+ const product = event.data.object;
1187
+ this.config.logger?.info(
1188
+ `Received webhook ${event.id}: ${event.type} for product ${product.id}`
1189
+ );
1190
+ await this.deleteProduct(product.id);
1191
+ break;
1192
+ }
1193
+ case "price.created":
1194
+ case "price.updated": {
1195
+ try {
1196
+ const { entity: price, refetched } = await this.fetchOrUseWebhookData(
1197
+ event.data.object,
1198
+ (id) => this.stripe.prices.retrieve(id)
1199
+ );
1200
+ this.config.logger?.info(
1201
+ `Received webhook ${event.id}: ${event.type} for price ${price.id}`
1202
+ );
1203
+ await this.upsertPrices([price], false, this.getSyncTimestamp(event, refetched));
1204
+ } catch (err) {
1205
+ if (err instanceof Stripe.errors.StripeAPIError && err.code === "resource_missing") {
1206
+ await this.deletePrice(event.data.object.id);
1207
+ } else {
1208
+ throw err;
1209
+ }
1210
+ }
1211
+ break;
1212
+ }
1213
+ case "price.deleted": {
1214
+ const price = event.data.object;
1215
+ this.config.logger?.info(
1216
+ `Received webhook ${event.id}: ${event.type} for price ${price.id}`
1217
+ );
1218
+ await this.deletePrice(price.id);
1219
+ break;
1220
+ }
1221
+ case "plan.created":
1222
+ case "plan.updated": {
1223
+ try {
1224
+ const { entity: plan, refetched } = await this.fetchOrUseWebhookData(
1225
+ event.data.object,
1226
+ (id) => this.stripe.plans.retrieve(id)
1227
+ );
1228
+ this.config.logger?.info(
1229
+ `Received webhook ${event.id}: ${event.type} for plan ${plan.id}`
1230
+ );
1231
+ await this.upsertPlans([plan], false, this.getSyncTimestamp(event, refetched));
1232
+ } catch (err) {
1233
+ if (err instanceof Stripe.errors.StripeAPIError && err.code === "resource_missing") {
1234
+ await this.deletePlan(event.data.object.id);
1235
+ } else {
1236
+ throw err;
1237
+ }
1238
+ }
1239
+ break;
1240
+ }
1241
+ case "plan.deleted": {
1242
+ const plan = event.data.object;
1243
+ this.config.logger?.info(`Received webhook ${event.id}: ${event.type} for plan ${plan.id}`);
1244
+ await this.deletePlan(plan.id);
1245
+ break;
1246
+ }
1247
+ case "setup_intent.canceled":
1248
+ case "setup_intent.created":
1249
+ case "setup_intent.requires_action":
1250
+ case "setup_intent.setup_failed":
1251
+ case "setup_intent.succeeded": {
1252
+ const { entity: setupIntent, refetched } = await this.fetchOrUseWebhookData(
1253
+ event.data.object,
1254
+ (id) => this.stripe.setupIntents.retrieve(id),
1255
+ (setupIntent2) => setupIntent2.status === "canceled" || setupIntent2.status === "succeeded"
1256
+ );
1257
+ this.config.logger?.info(
1258
+ `Received webhook ${event.id}: ${event.type} for setupIntent ${setupIntent.id}`
1259
+ );
1260
+ await this.upsertSetupIntents([setupIntent], false, this.getSyncTimestamp(event, refetched));
1261
+ break;
1262
+ }
1263
+ case "subscription_schedule.aborted":
1264
+ case "subscription_schedule.canceled":
1265
+ case "subscription_schedule.completed":
1266
+ case "subscription_schedule.created":
1267
+ case "subscription_schedule.expiring":
1268
+ case "subscription_schedule.released":
1269
+ case "subscription_schedule.updated": {
1270
+ const { entity: subscriptionSchedule, refetched } = await this.fetchOrUseWebhookData(
1271
+ event.data.object,
1272
+ (id) => this.stripe.subscriptionSchedules.retrieve(id),
1273
+ (schedule) => schedule.status === "canceled" || schedule.status === "completed"
1274
+ );
1275
+ this.config.logger?.info(
1276
+ `Received webhook ${event.id}: ${event.type} for subscriptionSchedule ${subscriptionSchedule.id}`
1277
+ );
1278
+ await this.upsertSubscriptionSchedules(
1279
+ [subscriptionSchedule],
1280
+ false,
1281
+ this.getSyncTimestamp(event, refetched)
1282
+ );
1283
+ break;
1284
+ }
1285
+ case "payment_method.attached":
1286
+ case "payment_method.automatically_updated":
1287
+ case "payment_method.detached":
1288
+ case "payment_method.updated": {
1289
+ const { entity: paymentMethod, refetched } = await this.fetchOrUseWebhookData(
1290
+ event.data.object,
1291
+ (id) => this.stripe.paymentMethods.retrieve(id)
1292
+ );
1293
+ this.config.logger?.info(
1294
+ `Received webhook ${event.id}: ${event.type} for paymentMethod ${paymentMethod.id}`
1295
+ );
1296
+ await this.upsertPaymentMethods(
1297
+ [paymentMethod],
1298
+ false,
1299
+ this.getSyncTimestamp(event, refetched)
1300
+ );
1301
+ break;
1302
+ }
1303
+ case "charge.dispute.created":
1304
+ case "charge.dispute.funds_reinstated":
1305
+ case "charge.dispute.funds_withdrawn":
1306
+ case "charge.dispute.updated":
1307
+ case "charge.dispute.closed": {
1308
+ const { entity: dispute, refetched } = await this.fetchOrUseWebhookData(
1309
+ event.data.object,
1310
+ (id) => this.stripe.disputes.retrieve(id),
1311
+ (dispute2) => dispute2.status === "won" || dispute2.status === "lost"
1312
+ );
1313
+ this.config.logger?.info(
1314
+ `Received webhook ${event.id}: ${event.type} for dispute ${dispute.id}`
1315
+ );
1316
+ await this.upsertDisputes([dispute], false, this.getSyncTimestamp(event, refetched));
1317
+ break;
1318
+ }
1319
+ case "payment_intent.amount_capturable_updated":
1320
+ case "payment_intent.canceled":
1321
+ case "payment_intent.created":
1322
+ case "payment_intent.partially_funded":
1323
+ case "payment_intent.payment_failed":
1324
+ case "payment_intent.processing":
1325
+ case "payment_intent.requires_action":
1326
+ case "payment_intent.succeeded": {
1327
+ const { entity: paymentIntent, refetched } = await this.fetchOrUseWebhookData(
1328
+ event.data.object,
1329
+ (id) => this.stripe.paymentIntents.retrieve(id),
1330
+ // Final states - do not re-fetch from API
1331
+ (entity) => entity.status === "canceled" || entity.status === "succeeded"
1332
+ );
1333
+ this.config.logger?.info(
1334
+ `Received webhook ${event.id}: ${event.type} for paymentIntent ${paymentIntent.id}`
1335
+ );
1336
+ await this.upsertPaymentIntents(
1337
+ [paymentIntent],
1338
+ false,
1339
+ this.getSyncTimestamp(event, refetched)
1340
+ );
1341
+ break;
1342
+ }
1343
+ case "credit_note.created":
1344
+ case "credit_note.updated":
1345
+ case "credit_note.voided": {
1346
+ const { entity: creditNote, refetched } = await this.fetchOrUseWebhookData(
1347
+ event.data.object,
1348
+ (id) => this.stripe.creditNotes.retrieve(id),
1349
+ (creditNote2) => creditNote2.status === "void"
1350
+ );
1351
+ this.config.logger?.info(
1352
+ `Received webhook ${event.id}: ${event.type} for creditNote ${creditNote.id}`
1353
+ );
1354
+ await this.upsertCreditNotes([creditNote], false, this.getSyncTimestamp(event, refetched));
1355
+ break;
1356
+ }
1357
+ case "radar.early_fraud_warning.created":
1358
+ case "radar.early_fraud_warning.updated": {
1359
+ const { entity: earlyFraudWarning, refetched } = await this.fetchOrUseWebhookData(
1360
+ event.data.object,
1361
+ (id) => this.stripe.radar.earlyFraudWarnings.retrieve(id)
1362
+ );
1363
+ this.config.logger?.info(
1364
+ `Received webhook ${event.id}: ${event.type} for earlyFraudWarning ${earlyFraudWarning.id}`
1365
+ );
1366
+ await this.upsertEarlyFraudWarning(
1367
+ [earlyFraudWarning],
1368
+ false,
1369
+ this.getSyncTimestamp(event, refetched)
1370
+ );
1371
+ break;
1372
+ }
1373
+ case "refund.created":
1374
+ case "refund.failed":
1375
+ case "refund.updated":
1376
+ case "charge.refund.updated": {
1377
+ const { entity: refund, refetched } = await this.fetchOrUseWebhookData(
1378
+ event.data.object,
1379
+ (id) => this.stripe.refunds.retrieve(id)
1380
+ );
1381
+ this.config.logger?.info(
1382
+ `Received webhook ${event.id}: ${event.type} for refund ${refund.id}`
1383
+ );
1384
+ await this.upsertRefunds([refund], false, this.getSyncTimestamp(event, refetched));
1385
+ break;
1386
+ }
1387
+ case "review.closed":
1388
+ case "review.opened": {
1389
+ const { entity: review, refetched } = await this.fetchOrUseWebhookData(
1390
+ event.data.object,
1391
+ (id) => this.stripe.reviews.retrieve(id)
1392
+ );
1393
+ this.config.logger?.info(
1394
+ `Received webhook ${event.id}: ${event.type} for review ${review.id}`
1395
+ );
1396
+ await this.upsertReviews([review], false, this.getSyncTimestamp(event, refetched));
1397
+ break;
1398
+ }
1399
+ case "entitlements.active_entitlement_summary.updated": {
1400
+ const activeEntitlementSummary = event.data.object;
1401
+ let entitlements = activeEntitlementSummary.entitlements;
1402
+ let refetched = false;
1403
+ if (this.config.revalidateObjectsViaStripeApi?.includes("entitlements")) {
1404
+ const { lastResponse, ...rest } = await this.stripe.entitlements.activeEntitlements.list({
1405
+ customer: activeEntitlementSummary.customer
1406
+ });
1407
+ entitlements = rest;
1408
+ refetched = true;
1409
+ }
1410
+ this.config.logger?.info(
1411
+ `Received webhook ${event.id}: ${event.type} for activeEntitlementSummary for customer ${activeEntitlementSummary.customer}`
1412
+ );
1413
+ await this.deleteRemovedActiveEntitlements(
1414
+ activeEntitlementSummary.customer,
1415
+ entitlements.data.map((entitlement) => entitlement.id)
1416
+ );
1417
+ await this.upsertActiveEntitlements(
1418
+ activeEntitlementSummary.customer,
1419
+ entitlements.data,
1420
+ false,
1421
+ this.getSyncTimestamp(event, refetched)
1422
+ );
1423
+ break;
1424
+ }
1425
+ default:
1426
+ throw new Error("Unhandled webhook event");
1427
+ }
1428
+ }
1429
+ getSyncTimestamp(event, refetched) {
1430
+ return refetched ? (/* @__PURE__ */ new Date()).toISOString() : new Date(event.created * 1e3).toISOString();
1431
+ }
1432
+ shouldRefetchEntity(entity) {
1433
+ return this.config.revalidateObjectsViaStripeApi?.includes(entity.object);
1434
+ }
1435
+ async fetchOrUseWebhookData(entity, fetchFn, entityInFinalState) {
1436
+ if (!entity.id) return { entity, refetched: false };
1437
+ if (entityInFinalState && entityInFinalState(entity)) return { entity, refetched: false };
1438
+ if (this.shouldRefetchEntity(entity)) {
1439
+ const fetchedEntity = await fetchFn(entity.id);
1440
+ return { entity: fetchedEntity, refetched: true };
1441
+ }
1442
+ return { entity, refetched: false };
1443
+ }
1444
+ async syncSingleEntity(stripeId) {
1445
+ if (stripeId.startsWith("cus_")) {
1446
+ return this.stripe.customers.retrieve(stripeId).then((it) => {
1447
+ if (!it || it.deleted) return;
1448
+ return this.upsertCustomers([it]);
1449
+ });
1450
+ } else if (stripeId.startsWith("in_")) {
1451
+ return this.stripe.invoices.retrieve(stripeId).then((it) => this.upsertInvoices([it]));
1452
+ } else if (stripeId.startsWith("price_")) {
1453
+ return this.stripe.prices.retrieve(stripeId).then((it) => this.upsertPrices([it]));
1454
+ } else if (stripeId.startsWith("prod_")) {
1455
+ return this.stripe.products.retrieve(stripeId).then((it) => this.upsertProducts([it]));
1456
+ } else if (stripeId.startsWith("sub_")) {
1457
+ return this.stripe.subscriptions.retrieve(stripeId).then((it) => this.upsertSubscriptions([it]));
1458
+ } else if (stripeId.startsWith("seti_")) {
1459
+ return this.stripe.setupIntents.retrieve(stripeId).then((it) => this.upsertSetupIntents([it]));
1460
+ } else if (stripeId.startsWith("pm_")) {
1461
+ return this.stripe.paymentMethods.retrieve(stripeId).then((it) => this.upsertPaymentMethods([it]));
1462
+ } else if (stripeId.startsWith("dp_") || stripeId.startsWith("du_")) {
1463
+ return this.stripe.disputes.retrieve(stripeId).then((it) => this.upsertDisputes([it]));
1464
+ } else if (stripeId.startsWith("ch_")) {
1465
+ return this.stripe.charges.retrieve(stripeId).then((it) => this.upsertCharges([it], true));
1466
+ } else if (stripeId.startsWith("pi_")) {
1467
+ return this.stripe.paymentIntents.retrieve(stripeId).then((it) => this.upsertPaymentIntents([it]));
1468
+ } else if (stripeId.startsWith("txi_")) {
1469
+ return this.stripe.taxIds.retrieve(stripeId).then((it) => this.upsertTaxIds([it]));
1470
+ } else if (stripeId.startsWith("cn_")) {
1471
+ return this.stripe.creditNotes.retrieve(stripeId).then((it) => this.upsertCreditNotes([it]));
1472
+ } else if (stripeId.startsWith("issfr_")) {
1473
+ return this.stripe.radar.earlyFraudWarnings.retrieve(stripeId).then((it) => this.upsertEarlyFraudWarning([it]));
1474
+ } else if (stripeId.startsWith("prv_")) {
1475
+ return this.stripe.reviews.retrieve(stripeId).then((it) => this.upsertReviews([it]));
1476
+ } else if (stripeId.startsWith("re_")) {
1477
+ return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it]));
1478
+ } else if (stripeId.startsWith("feat_")) {
1479
+ return this.stripe.entitlements.features.retrieve(stripeId).then((it) => this.upsertFeatures([it]));
1480
+ } else if (stripeId.startsWith("cs_")) {
1481
+ return this.stripe.checkout.sessions.retrieve(stripeId).then((it) => this.upsertCheckoutSessions([it]));
1482
+ }
1483
+ }
1484
+ async syncBackfill(params) {
1485
+ const { object } = params ?? {};
1486
+ let products, prices, customers, checkoutSessions, subscriptions, subscriptionSchedules, invoices, setupIntents, paymentMethods, disputes, charges, paymentIntents, plans, taxIds, creditNotes, earlyFraudWarnings, refunds;
1487
+ switch (object) {
1488
+ case "all":
1489
+ products = await this.syncProducts(params);
1490
+ prices = await this.syncPrices(params);
1491
+ plans = await this.syncPlans(params);
1492
+ customers = await this.syncCustomers(params);
1493
+ subscriptions = await this.syncSubscriptions(params);
1494
+ subscriptionSchedules = await this.syncSubscriptionSchedules(params);
1495
+ invoices = await this.syncInvoices(params);
1496
+ charges = await this.syncCharges(params);
1497
+ setupIntents = await this.syncSetupIntents(params);
1498
+ paymentMethods = await this.syncPaymentMethods(params);
1499
+ paymentIntents = await this.syncPaymentIntents(params);
1500
+ taxIds = await this.syncTaxIds(params);
1501
+ creditNotes = await this.syncCreditNotes(params);
1502
+ disputes = await this.syncDisputes(params);
1503
+ earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
1504
+ refunds = await this.syncRefunds(params);
1505
+ checkoutSessions = await this.syncCheckoutSessions(params);
1506
+ break;
1507
+ case "customer":
1508
+ customers = await this.syncCustomers(params);
1509
+ break;
1510
+ case "invoice":
1511
+ invoices = await this.syncInvoices(params);
1512
+ break;
1513
+ case "price":
1514
+ prices = await this.syncPrices(params);
1515
+ break;
1516
+ case "product":
1517
+ products = await this.syncProducts(params);
1518
+ break;
1519
+ case "subscription":
1520
+ subscriptions = await this.syncSubscriptions(params);
1521
+ break;
1522
+ case "subscription_schedules":
1523
+ subscriptionSchedules = await this.syncSubscriptionSchedules(params);
1524
+ break;
1525
+ case "setup_intent":
1526
+ setupIntents = await this.syncSetupIntents(params);
1527
+ break;
1528
+ case "payment_method":
1529
+ paymentMethods = await this.syncPaymentMethods(params);
1530
+ break;
1531
+ case "dispute":
1532
+ disputes = await this.syncDisputes(params);
1533
+ break;
1534
+ case "charge":
1535
+ charges = await this.syncCharges(params);
1536
+ break;
1537
+ case "payment_intent":
1538
+ paymentIntents = await this.syncPaymentIntents(params);
1539
+ case "plan":
1540
+ plans = await this.syncPlans(params);
1541
+ break;
1542
+ case "tax_id":
1543
+ taxIds = await this.syncTaxIds(params);
1544
+ break;
1545
+ case "credit_note":
1546
+ creditNotes = await this.syncCreditNotes(params);
1547
+ break;
1548
+ case "early_fraud_warning":
1549
+ earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
1550
+ break;
1551
+ case "refund":
1552
+ refunds = await this.syncRefunds(params);
1553
+ break;
1554
+ case "checkout_sessions":
1555
+ checkoutSessions = await this.syncCheckoutSessions(params);
1556
+ break;
1557
+ default:
1558
+ break;
1559
+ }
1560
+ return {
1561
+ products,
1562
+ prices,
1563
+ customers,
1564
+ checkoutSessions,
1565
+ subscriptions,
1566
+ subscriptionSchedules,
1567
+ invoices,
1568
+ setupIntents,
1569
+ paymentMethods,
1570
+ disputes,
1571
+ charges,
1572
+ paymentIntents,
1573
+ plans,
1574
+ taxIds,
1575
+ creditNotes,
1576
+ earlyFraudWarnings,
1577
+ refunds
1578
+ };
1579
+ }
1580
+ async syncProducts(syncParams) {
1581
+ this.config.logger?.info("Syncing products");
1582
+ const params = { limit: 100 };
1583
+ if (syncParams?.created) params.created = syncParams?.created;
1584
+ return this.fetchAndUpsert(
1585
+ () => this.stripe.products.list(params),
1586
+ (products) => this.upsertProducts(products)
1587
+ );
1588
+ }
1589
+ async syncPrices(syncParams) {
1590
+ this.config.logger?.info("Syncing prices");
1591
+ const params = { limit: 100 };
1592
+ if (syncParams?.created) params.created = syncParams?.created;
1593
+ return this.fetchAndUpsert(
1594
+ () => this.stripe.prices.list(params),
1595
+ (prices) => this.upsertPrices(prices, syncParams?.backfillRelatedEntities)
1596
+ );
1597
+ }
1598
+ async syncPlans(syncParams) {
1599
+ this.config.logger?.info("Syncing plans");
1600
+ const params = { limit: 100 };
1601
+ if (syncParams?.created) params.created = syncParams?.created;
1602
+ return this.fetchAndUpsert(
1603
+ () => this.stripe.plans.list(params),
1604
+ (plans) => this.upsertPlans(plans, syncParams?.backfillRelatedEntities)
1605
+ );
1606
+ }
1607
+ async syncCustomers(syncParams) {
1608
+ this.config.logger?.info("Syncing customers");
1609
+ const params = { limit: 100 };
1610
+ if (syncParams?.created) params.created = syncParams.created;
1611
+ return this.fetchAndUpsert(
1612
+ () => this.stripe.customers.list(params),
1613
+ // @ts-expect-error
1614
+ (items) => this.upsertCustomers(items)
1615
+ );
1616
+ }
1617
+ async syncSubscriptions(syncParams) {
1618
+ this.config.logger?.info("Syncing subscriptions");
1619
+ const params = { status: "all", limit: 100 };
1620
+ if (syncParams?.created) params.created = syncParams.created;
1621
+ return this.fetchAndUpsert(
1622
+ () => this.stripe.subscriptions.list(params),
1623
+ (items) => this.upsertSubscriptions(items, syncParams?.backfillRelatedEntities)
1624
+ );
1625
+ }
1626
+ async syncSubscriptionSchedules(syncParams) {
1627
+ this.config.logger?.info("Syncing subscription schedules");
1628
+ const params = { limit: 100 };
1629
+ if (syncParams?.created) params.created = syncParams.created;
1630
+ return this.fetchAndUpsert(
1631
+ () => this.stripe.subscriptionSchedules.list(params),
1632
+ (items) => this.upsertSubscriptionSchedules(items, syncParams?.backfillRelatedEntities)
1633
+ );
1634
+ }
1635
+ async syncInvoices(syncParams) {
1636
+ this.config.logger?.info("Syncing invoices");
1637
+ const params = { limit: 100 };
1638
+ if (syncParams?.created) params.created = syncParams.created;
1639
+ return this.fetchAndUpsert(
1640
+ () => this.stripe.invoices.list(params),
1641
+ (items) => this.upsertInvoices(items, syncParams?.backfillRelatedEntities)
1642
+ );
1643
+ }
1644
+ async syncCharges(syncParams) {
1645
+ this.config.logger?.info("Syncing charges");
1646
+ const params = { limit: 100 };
1647
+ if (syncParams?.created) params.created = syncParams.created;
1648
+ return this.fetchAndUpsert(
1649
+ () => this.stripe.charges.list(params),
1650
+ (items) => this.upsertCharges(items, syncParams?.backfillRelatedEntities)
1651
+ );
1652
+ }
1653
+ async syncSetupIntents(syncParams) {
1654
+ this.config.logger?.info("Syncing setup_intents");
1655
+ const params = { limit: 100 };
1656
+ if (syncParams?.created) params.created = syncParams.created;
1657
+ return this.fetchAndUpsert(
1658
+ () => this.stripe.setupIntents.list(params),
1659
+ (items) => this.upsertSetupIntents(items, syncParams?.backfillRelatedEntities)
1660
+ );
1661
+ }
1662
+ async syncPaymentIntents(syncParams) {
1663
+ this.config.logger?.info("Syncing payment_intents");
1664
+ const params = { limit: 100 };
1665
+ if (syncParams?.created) params.created = syncParams.created;
1666
+ return this.fetchAndUpsert(
1667
+ () => this.stripe.paymentIntents.list(params),
1668
+ (items) => this.upsertPaymentIntents(items, syncParams?.backfillRelatedEntities)
1669
+ );
1670
+ }
1671
+ async syncTaxIds(syncParams) {
1672
+ this.config.logger?.info("Syncing tax_ids");
1673
+ const params = { limit: 100 };
1674
+ return this.fetchAndUpsert(
1675
+ () => this.stripe.taxIds.list(params),
1676
+ (items) => this.upsertTaxIds(items, syncParams?.backfillRelatedEntities)
1677
+ );
1678
+ }
1679
+ async syncPaymentMethods(syncParams) {
1680
+ this.config.logger?.info("Syncing payment method");
1681
+ const prepared = sql2(
1682
+ `select id from "${this.config.schema}"."customers" WHERE deleted <> true;`
1683
+ )([]);
1684
+ const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
1685
+ this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
1686
+ let synced = 0;
1687
+ for (const customerIdChunk of chunkArray(customerIds, 10)) {
1688
+ await Promise.all(
1689
+ customerIdChunk.map(async (customerId) => {
1690
+ const syncResult = await this.fetchAndUpsert(
1691
+ () => this.stripe.paymentMethods.list({
1692
+ limit: 100,
1693
+ customer: customerId
1694
+ }),
1695
+ (items) => this.upsertPaymentMethods(items, syncParams?.backfillRelatedEntities)
1696
+ );
1697
+ synced += syncResult.synced;
1698
+ })
1699
+ );
1700
+ }
1701
+ return { synced };
1702
+ }
1703
+ async syncDisputes(syncParams) {
1704
+ const params = { limit: 100 };
1705
+ if (syncParams?.created) params.created = syncParams.created;
1706
+ return this.fetchAndUpsert(
1707
+ () => this.stripe.disputes.list(params),
1708
+ (items) => this.upsertDisputes(items, syncParams?.backfillRelatedEntities)
1709
+ );
1710
+ }
1711
+ async syncEarlyFraudWarnings(syncParams) {
1712
+ this.config.logger?.info("Syncing early fraud warnings");
1713
+ const params = { limit: 100 };
1714
+ if (syncParams?.created) params.created = syncParams.created;
1715
+ return this.fetchAndUpsert(
1716
+ () => this.stripe.radar.earlyFraudWarnings.list(params),
1717
+ (items) => this.upsertEarlyFraudWarning(items, syncParams?.backfillRelatedEntities)
1718
+ );
1719
+ }
1720
+ async syncRefunds(syncParams) {
1721
+ this.config.logger?.info("Syncing refunds");
1722
+ const params = { limit: 100 };
1723
+ if (syncParams?.created) params.created = syncParams.created;
1724
+ return this.fetchAndUpsert(
1725
+ () => this.stripe.refunds.list(params),
1726
+ (items) => this.upsertRefunds(items, syncParams?.backfillRelatedEntities)
1727
+ );
1728
+ }
1729
+ async syncCreditNotes(syncParams) {
1730
+ this.config.logger?.info("Syncing credit notes");
1731
+ const params = { limit: 100 };
1732
+ if (syncParams?.created) params.created = syncParams?.created;
1733
+ return this.fetchAndUpsert(
1734
+ () => this.stripe.creditNotes.list(params),
1735
+ (creditNotes) => this.upsertCreditNotes(creditNotes)
1736
+ );
1737
+ }
1738
+ async syncFeatures(syncParams) {
1739
+ this.config.logger?.info("Syncing features");
1740
+ const params = { limit: 100, ...syncParams?.pagination };
1741
+ return this.fetchAndUpsert(
1742
+ () => this.stripe.entitlements.features.list(params),
1743
+ (features) => this.upsertFeatures(features)
1744
+ );
1745
+ }
1746
+ async syncEntitlements(customerId, syncParams) {
1747
+ this.config.logger?.info("Syncing entitlements");
1748
+ const params = {
1749
+ customer: customerId,
1750
+ limit: 100,
1751
+ ...syncParams?.pagination
1752
+ };
1753
+ return this.fetchAndUpsert(
1754
+ () => this.stripe.entitlements.activeEntitlements.list(params),
1755
+ (entitlements) => this.upsertActiveEntitlements(customerId, entitlements)
1756
+ );
1757
+ }
1758
+ async syncCheckoutSessions(syncParams) {
1759
+ this.config.logger?.info("Syncing checkout sessions");
1760
+ const params = {
1761
+ limit: 100
1762
+ };
1763
+ if (syncParams?.created) params.created = syncParams.created;
1764
+ return this.fetchAndUpsert(
1765
+ () => this.stripe.checkout.sessions.list(params),
1766
+ (items) => this.upsertCheckoutSessions(items, syncParams?.backfillRelatedEntities)
1767
+ );
1768
+ }
1769
+ async fetchAndUpsert(fetch, upsert) {
1770
+ const items = [];
1771
+ this.config.logger?.info("Fetching items to sync from Stripe");
1772
+ for await (const item of fetch()) {
1773
+ items.push(item);
1774
+ }
1775
+ if (!items.length) return { synced: 0 };
1776
+ this.config.logger?.info(`Upserting ${items.length} items`);
1777
+ const chunkSize = 250;
1778
+ for (let i = 0; i < items.length; i += chunkSize) {
1779
+ const chunk = items.slice(i, i + chunkSize);
1780
+ await upsert(chunk);
1781
+ }
1782
+ this.config.logger?.info("Upserted items");
1783
+ return { synced: items.length };
1784
+ }
1785
+ async upsertCharges(charges, backfillRelatedEntities, syncTimestamp) {
1786
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1787
+ await Promise.all([
1788
+ this.backfillCustomers(getUniqueIds(charges, "customer")),
1789
+ this.backfillInvoices(getUniqueIds(charges, "invoice"))
1790
+ ]);
1791
+ }
1792
+ await this.expandEntity(
1793
+ charges,
1794
+ "refunds",
1795
+ (id) => this.stripe.refunds.list({ charge: id, limit: 100 })
1796
+ );
1797
+ return this.postgresClient.upsertManyWithTimestampProtection(
1798
+ charges,
1799
+ "charges",
1800
+ chargeSchema,
1801
+ syncTimestamp
1802
+ );
1803
+ }
1804
+ async backfillCharges(chargeIds) {
1805
+ const missingChargeIds = await this.postgresClient.findMissingEntries("charges", chargeIds);
1806
+ await this.fetchMissingEntities(
1807
+ missingChargeIds,
1808
+ (id) => this.stripe.charges.retrieve(id)
1809
+ ).then((charges) => this.upsertCharges(charges));
1810
+ }
1811
+ async backfillPaymentIntents(paymentIntentIds) {
1812
+ const missingIds = await this.postgresClient.findMissingEntries(
1813
+ "payment_intents",
1814
+ paymentIntentIds
1815
+ );
1816
+ await this.fetchMissingEntities(
1817
+ missingIds,
1818
+ (id) => this.stripe.paymentIntents.retrieve(id)
1819
+ ).then((paymentIntents) => this.upsertPaymentIntents(paymentIntents));
1820
+ }
1821
+ async upsertCreditNotes(creditNotes, backfillRelatedEntities, syncTimestamp) {
1822
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1823
+ await Promise.all([
1824
+ this.backfillCustomers(getUniqueIds(creditNotes, "customer")),
1825
+ this.backfillInvoices(getUniqueIds(creditNotes, "invoice"))
1826
+ ]);
1827
+ }
1828
+ await this.expandEntity(
1829
+ creditNotes,
1830
+ "lines",
1831
+ (id) => this.stripe.creditNotes.listLineItems(id, { limit: 100 })
1832
+ );
1833
+ return this.postgresClient.upsertManyWithTimestampProtection(
1834
+ creditNotes,
1835
+ "credit_notes",
1836
+ creditNoteSchema,
1837
+ syncTimestamp
1838
+ );
1839
+ }
1840
+ async upsertCheckoutSessions(checkoutSessions, backfillRelatedEntities, syncTimestamp) {
1841
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1842
+ await Promise.all([
1843
+ this.backfillCustomers(getUniqueIds(checkoutSessions, "customer")),
1844
+ this.backfillSubscriptions(getUniqueIds(checkoutSessions, "subscription")),
1845
+ this.backfillPaymentIntents(getUniqueIds(checkoutSessions, "payment_intent")),
1846
+ this.backfillInvoices(getUniqueIds(checkoutSessions, "invoice"))
1847
+ ]);
1848
+ }
1849
+ const rows = await this.postgresClient.upsertManyWithTimestampProtection(
1850
+ checkoutSessions,
1851
+ "checkout_sessions",
1852
+ checkoutSessionSchema,
1853
+ syncTimestamp
1854
+ );
1855
+ await this.fillCheckoutSessionsLineItems(
1856
+ checkoutSessions.map((cs) => cs.id),
1857
+ syncTimestamp
1858
+ );
1859
+ return rows;
1860
+ }
1861
+ async upsertEarlyFraudWarning(earlyFraudWarnings, backfillRelatedEntities, syncTimestamp) {
1862
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1863
+ await Promise.all([
1864
+ this.backfillPaymentIntents(getUniqueIds(earlyFraudWarnings, "payment_intent")),
1865
+ this.backfillCharges(getUniqueIds(earlyFraudWarnings, "charge"))
1866
+ ]);
1867
+ }
1868
+ return this.postgresClient.upsertManyWithTimestampProtection(
1869
+ earlyFraudWarnings,
1870
+ "early_fraud_warnings",
1871
+ earlyFraudWarningSchema,
1872
+ syncTimestamp
1873
+ );
1874
+ }
1875
+ async upsertRefunds(refunds, backfillRelatedEntities, syncTimestamp) {
1876
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1877
+ await Promise.all([
1878
+ this.backfillPaymentIntents(getUniqueIds(refunds, "payment_intent")),
1879
+ this.backfillCharges(getUniqueIds(refunds, "charge"))
1880
+ ]);
1881
+ }
1882
+ return this.postgresClient.upsertManyWithTimestampProtection(
1883
+ refunds,
1884
+ "refunds",
1885
+ refundSchema,
1886
+ syncTimestamp
1887
+ );
1888
+ }
1889
+ async upsertReviews(reviews, backfillRelatedEntities, syncTimestamp) {
1890
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1891
+ await Promise.all([
1892
+ this.backfillPaymentIntents(getUniqueIds(reviews, "payment_intent")),
1893
+ this.backfillCharges(getUniqueIds(reviews, "charge"))
1894
+ ]);
1895
+ }
1896
+ return this.postgresClient.upsertManyWithTimestampProtection(
1897
+ reviews,
1898
+ "reviews",
1899
+ reviewSchema,
1900
+ syncTimestamp
1901
+ );
1902
+ }
1903
+ async upsertCustomers(customers, syncTimestamp) {
1904
+ const deletedCustomers = customers.filter((customer) => customer.deleted);
1905
+ const nonDeletedCustomers = customers.filter((customer) => !customer.deleted);
1906
+ await this.postgresClient.upsertManyWithTimestampProtection(
1907
+ nonDeletedCustomers,
1908
+ "customers",
1909
+ customerSchema,
1910
+ syncTimestamp
1911
+ );
1912
+ await this.postgresClient.upsertManyWithTimestampProtection(
1913
+ deletedCustomers,
1914
+ "customers",
1915
+ customerDeletedSchema,
1916
+ syncTimestamp
1917
+ );
1918
+ return customers;
1919
+ }
1920
+ async backfillCustomers(customerIds) {
1921
+ const missingIds = await this.postgresClient.findMissingEntries("customers", customerIds);
1922
+ await this.fetchMissingEntities(missingIds, (id) => this.stripe.customers.retrieve(id)).then((entries) => this.upsertCustomers(entries)).catch((err) => {
1923
+ this.config.logger?.error(err, "Failed to backfill");
1924
+ throw err;
1925
+ });
1926
+ }
1927
+ async upsertDisputes(disputes, backfillRelatedEntities, syncTimestamp) {
1928
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1929
+ await this.backfillCharges(getUniqueIds(disputes, "charge"));
1930
+ }
1931
+ return this.postgresClient.upsertManyWithTimestampProtection(
1932
+ disputes,
1933
+ "disputes",
1934
+ disputeSchema,
1935
+ syncTimestamp
1936
+ );
1937
+ }
1938
+ async upsertInvoices(invoices, backfillRelatedEntities, syncTimestamp) {
1939
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1940
+ await Promise.all([
1941
+ this.backfillCustomers(getUniqueIds(invoices, "customer")),
1942
+ this.backfillSubscriptions(getUniqueIds(invoices, "subscription"))
1943
+ ]);
1944
+ }
1945
+ await this.expandEntity(
1946
+ invoices,
1947
+ "lines",
1948
+ (id) => this.stripe.invoices.listLineItems(id, { limit: 100 })
1949
+ );
1950
+ return this.postgresClient.upsertManyWithTimestampProtection(
1951
+ invoices,
1952
+ "invoices",
1953
+ invoiceSchema,
1954
+ syncTimestamp
1955
+ );
1956
+ }
1957
+ backfillInvoices = async (invoiceIds) => {
1958
+ const missingIds = await this.postgresClient.findMissingEntries("invoices", invoiceIds);
1959
+ await this.fetchMissingEntities(missingIds, (id) => this.stripe.invoices.retrieve(id)).then(
1960
+ (entries) => this.upsertInvoices(entries)
1961
+ );
1962
+ };
1963
+ backfillPrices = async (priceIds) => {
1964
+ const missingIds = await this.postgresClient.findMissingEntries("prices", priceIds);
1965
+ await this.fetchMissingEntities(missingIds, (id) => this.stripe.prices.retrieve(id)).then(
1966
+ (entries) => this.upsertPrices(entries)
1967
+ );
1968
+ };
1969
+ async upsertPlans(plans, backfillRelatedEntities, syncTimestamp) {
1970
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1971
+ await this.backfillProducts(getUniqueIds(plans, "product"));
1972
+ }
1973
+ return this.postgresClient.upsertManyWithTimestampProtection(
1974
+ plans,
1975
+ "plans",
1976
+ planSchema,
1977
+ syncTimestamp
1978
+ );
1979
+ }
1980
+ async deletePlan(id) {
1981
+ return this.postgresClient.delete("plans", id);
1982
+ }
1983
+ async upsertPrices(prices, backfillRelatedEntities, syncTimestamp) {
1984
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1985
+ await this.backfillProducts(getUniqueIds(prices, "product"));
1986
+ }
1987
+ return this.postgresClient.upsertManyWithTimestampProtection(
1988
+ prices,
1989
+ "prices",
1990
+ priceSchema,
1991
+ syncTimestamp
1992
+ );
1993
+ }
1994
+ async deletePrice(id) {
1995
+ return this.postgresClient.delete("prices", id);
1996
+ }
1997
+ async upsertProducts(products, syncTimestamp) {
1998
+ return this.postgresClient.upsertManyWithTimestampProtection(
1999
+ products,
2000
+ "products",
2001
+ productSchema,
2002
+ syncTimestamp
2003
+ );
2004
+ }
2005
+ async deleteProduct(id) {
2006
+ return this.postgresClient.delete("products", id);
2007
+ }
2008
+ async backfillProducts(productIds) {
2009
+ const missingProductIds = await this.postgresClient.findMissingEntries("products", productIds);
2010
+ await this.fetchMissingEntities(
2011
+ missingProductIds,
2012
+ (id) => this.stripe.products.retrieve(id)
2013
+ ).then((products) => this.upsertProducts(products));
2014
+ }
2015
+ async upsertPaymentIntents(paymentIntents, backfillRelatedEntities, syncTimestamp) {
2016
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2017
+ await Promise.all([
2018
+ this.backfillCustomers(getUniqueIds(paymentIntents, "customer")),
2019
+ this.backfillInvoices(getUniqueIds(paymentIntents, "invoice"))
2020
+ ]);
2021
+ }
2022
+ return this.postgresClient.upsertManyWithTimestampProtection(
2023
+ paymentIntents,
2024
+ "payment_intents",
2025
+ paymentIntentSchema,
2026
+ syncTimestamp
2027
+ );
2028
+ }
2029
+ async upsertPaymentMethods(paymentMethods, backfillRelatedEntities = false, syncTimestamp) {
2030
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2031
+ await this.backfillCustomers(getUniqueIds(paymentMethods, "customer"));
2032
+ }
2033
+ return this.postgresClient.upsertManyWithTimestampProtection(
2034
+ paymentMethods,
2035
+ "payment_methods",
2036
+ paymentMethodsSchema,
2037
+ syncTimestamp
2038
+ );
2039
+ }
2040
+ async upsertSetupIntents(setupIntents, backfillRelatedEntities, syncTimestamp) {
2041
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2042
+ await this.backfillCustomers(getUniqueIds(setupIntents, "customer"));
2043
+ }
2044
+ return this.postgresClient.upsertManyWithTimestampProtection(
2045
+ setupIntents,
2046
+ "setup_intents",
2047
+ setupIntentsSchema,
2048
+ syncTimestamp
2049
+ );
2050
+ }
2051
+ async upsertTaxIds(taxIds, backfillRelatedEntities, syncTimestamp) {
2052
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2053
+ await this.backfillCustomers(getUniqueIds(taxIds, "customer"));
2054
+ }
2055
+ return this.postgresClient.upsertManyWithTimestampProtection(
2056
+ taxIds,
2057
+ "tax_ids",
2058
+ taxIdSchema,
2059
+ syncTimestamp
2060
+ );
2061
+ }
2062
+ async deleteTaxId(id) {
2063
+ return this.postgresClient.delete("tax_ids", id);
2064
+ }
2065
+ async upsertSubscriptionItems(subscriptionItems, syncTimestamp) {
2066
+ const modifiedSubscriptionItems = subscriptionItems.map((subscriptionItem) => {
2067
+ const priceId = subscriptionItem.price.id.toString();
2068
+ const deleted = subscriptionItem.deleted;
2069
+ const quantity = subscriptionItem.quantity;
2070
+ return {
2071
+ ...subscriptionItem,
2072
+ price: priceId,
2073
+ deleted: deleted ?? false,
2074
+ quantity: quantity ?? null
2075
+ };
2076
+ });
2077
+ await this.postgresClient.upsertManyWithTimestampProtection(
2078
+ modifiedSubscriptionItems,
2079
+ "subscription_items",
2080
+ subscriptionItemSchema,
2081
+ syncTimestamp
2082
+ );
2083
+ }
2084
+ async fillCheckoutSessionsLineItems(checkoutSessionIds, syncTimestamp) {
2085
+ for (const checkoutSessionId of checkoutSessionIds) {
2086
+ const lineItemResponses = [];
2087
+ for await (const lineItem of this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
2088
+ limit: 100
2089
+ })) {
2090
+ lineItemResponses.push(lineItem);
2091
+ }
2092
+ await this.upsertCheckoutSessionLineItems(lineItemResponses, checkoutSessionId, syncTimestamp);
2093
+ }
2094
+ }
2095
+ async upsertCheckoutSessionLineItems(lineItems, checkoutSessionId, syncTimestamp) {
2096
+ await this.backfillPrices(
2097
+ lineItems.map((lineItem) => lineItem.price?.id?.toString() ?? void 0).filter((id) => id !== void 0)
2098
+ );
2099
+ const modifiedLineItems = lineItems.map((lineItem) => {
2100
+ const priceId = typeof lineItem.price === "object" && lineItem.price?.id ? lineItem.price.id.toString() : lineItem.price?.toString() || null;
2101
+ return {
2102
+ ...lineItem,
2103
+ price: priceId,
2104
+ checkout_session: checkoutSessionId
2105
+ };
2106
+ });
2107
+ await this.postgresClient.upsertManyWithTimestampProtection(
2108
+ modifiedLineItems,
2109
+ "checkout_session_line_items",
2110
+ checkoutSessionLineItemSchema,
2111
+ syncTimestamp
2112
+ );
2113
+ }
2114
+ async markDeletedSubscriptionItems(subscriptionId, currentSubItemIds) {
2115
+ let prepared = sql2(`
2116
+ select id from "${this.config.schema}"."subscription_items"
2117
+ where subscription = :subscriptionId and deleted = false;
2118
+ `)({ subscriptionId });
2119
+ const { rows } = await this.postgresClient.query(prepared.text, prepared.values);
2120
+ const deletedIds = rows.filter(
2121
+ ({ id }) => currentSubItemIds.includes(id) === false
2122
+ );
2123
+ if (deletedIds.length > 0) {
2124
+ const ids = deletedIds.map(({ id }) => id);
2125
+ prepared = sql2(`
2126
+ update "${this.config.schema}"."subscription_items"
2127
+ set deleted = true where id=any(:ids::text[]);
2128
+ `)({ ids });
2129
+ const { rowCount } = await await this.postgresClient.query(prepared.text, prepared.values);
2130
+ return { rowCount: rowCount || 0 };
2131
+ } else {
2132
+ return { rowCount: 0 };
2133
+ }
2134
+ }
2135
+ async upsertSubscriptionSchedules(subscriptionSchedules, backfillRelatedEntities, syncTimestamp) {
2136
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2137
+ const customerIds = getUniqueIds(subscriptionSchedules, "customer");
2138
+ await this.backfillCustomers(customerIds);
2139
+ }
2140
+ const rows = await this.postgresClient.upsertManyWithTimestampProtection(
2141
+ subscriptionSchedules,
2142
+ "subscription_schedules",
2143
+ subscriptionScheduleSchema,
2144
+ syncTimestamp
2145
+ );
2146
+ return rows;
2147
+ }
2148
+ async upsertSubscriptions(subscriptions, backfillRelatedEntities, syncTimestamp) {
2149
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2150
+ const customerIds = getUniqueIds(subscriptions, "customer");
2151
+ await this.backfillCustomers(customerIds);
2152
+ }
2153
+ await this.expandEntity(
2154
+ subscriptions,
2155
+ "items",
2156
+ (id) => this.stripe.subscriptionItems.list({ subscription: id, limit: 100 })
2157
+ );
2158
+ const rows = await this.postgresClient.upsertManyWithTimestampProtection(
2159
+ subscriptions,
2160
+ "subscriptions",
2161
+ subscriptionSchema,
2162
+ syncTimestamp
2163
+ );
2164
+ const allSubscriptionItems = subscriptions.flatMap((subscription) => subscription.items.data);
2165
+ await this.upsertSubscriptionItems(allSubscriptionItems, syncTimestamp);
2166
+ const markSubscriptionItemsDeleted = [];
2167
+ for (const subscription of subscriptions) {
2168
+ const subscriptionItems = subscription.items.data;
2169
+ const subItemIds = subscriptionItems.map((x) => x.id);
2170
+ markSubscriptionItemsDeleted.push(
2171
+ this.markDeletedSubscriptionItems(subscription.id, subItemIds)
2172
+ );
2173
+ }
2174
+ await Promise.all(markSubscriptionItemsDeleted);
2175
+ return rows;
2176
+ }
2177
+ async deleteRemovedActiveEntitlements(customerId, currentActiveEntitlementIds) {
2178
+ const prepared = sql2(`
2179
+ delete from "${this.config.schema}"."active_entitlements"
2180
+ where customer = :customerId and id <> ALL(:currentActiveEntitlementIds::text[]);
2181
+ `)({ customerId, currentActiveEntitlementIds });
2182
+ const { rowCount } = await this.postgresClient.query(prepared.text, prepared.values);
2183
+ return { rowCount: rowCount || 0 };
2184
+ }
2185
+ async upsertFeatures(features, syncTimestamp) {
2186
+ return this.postgresClient.upsertManyWithTimestampProtection(
2187
+ features,
2188
+ "features",
2189
+ featureSchema,
2190
+ syncTimestamp
2191
+ );
2192
+ }
2193
+ async backfillFeatures(featureIds) {
2194
+ const missingFeatureIds = await this.postgresClient.findMissingEntries("features", featureIds);
2195
+ await this.fetchMissingEntities(
2196
+ missingFeatureIds,
2197
+ (id) => this.stripe.entitlements.features.retrieve(id)
2198
+ ).then((features) => this.upsertFeatures(features)).catch((err) => {
2199
+ this.config.logger?.error(err, "Failed to backfill features");
2200
+ throw err;
2201
+ });
2202
+ }
2203
+ async upsertActiveEntitlements(customerId, activeEntitlements, backfillRelatedEntities, syncTimestamp) {
2204
+ if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2205
+ await Promise.all([
2206
+ this.backfillCustomers(getUniqueIds(activeEntitlements, "customer")),
2207
+ this.backfillFeatures(getUniqueIds(activeEntitlements, "feature"))
2208
+ ]);
2209
+ }
2210
+ const entitlements = activeEntitlements.map((entitlement) => ({
2211
+ id: entitlement.id,
2212
+ object: entitlement.object,
2213
+ feature: typeof entitlement.feature === "string" ? entitlement.feature : entitlement.feature.id,
2214
+ customer: customerId,
2215
+ livemode: entitlement.livemode,
2216
+ lookup_key: entitlement.lookup_key
2217
+ }));
2218
+ return this.postgresClient.upsertManyWithTimestampProtection(
2219
+ entitlements,
2220
+ "active_entitlements",
2221
+ activeEntitlementSchema,
2222
+ syncTimestamp
2223
+ );
2224
+ }
2225
+ // Managed Webhook CRUD methods
2226
+ async createManagedWebhook(baseUrl, params) {
2227
+ const uuid = randomUUID();
2228
+ const webhookUrl = `${baseUrl}/${uuid}`;
2229
+ const webhook = await this.stripe.webhookEndpoints.create({
2230
+ ...params,
2231
+ url: webhookUrl
2232
+ });
2233
+ const webhookWithUuid = { ...webhook, uuid };
2234
+ await this.upsertManagedWebhooks([webhookWithUuid]);
2235
+ return { webhook, uuid };
2236
+ }
2237
+ async getManagedWebhook(id) {
2238
+ const result = await this.postgresClient.query(
2239
+ `SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" WHERE id = $1`,
2240
+ [id]
2241
+ );
2242
+ return result.rows.length > 0 ? result.rows[0] : null;
2243
+ }
2244
+ async listManagedWebhooks() {
2245
+ const result = await this.postgresClient.query(
2246
+ `SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" ORDER BY created DESC`
2247
+ );
2248
+ return result.rows;
2249
+ }
2250
+ async updateManagedWebhook(id, params) {
2251
+ const webhook = await this.stripe.webhookEndpoints.update(id, params);
2252
+ const existing = await this.getManagedWebhook(id);
2253
+ const webhookWithUuid = { ...webhook, uuid: existing?.uuid || randomUUID() };
2254
+ await this.upsertManagedWebhooks([webhookWithUuid]);
2255
+ return webhook;
2256
+ }
2257
+ async deleteManagedWebhook(id) {
2258
+ await this.stripe.webhookEndpoints.del(id);
2259
+ return this.postgresClient.delete("managed_webhooks", id);
2260
+ }
2261
+ async upsertManagedWebhooks(webhooks, syncTimestamp) {
2262
+ return this.postgresClient.upsertManyWithTimestampProtection(
2263
+ webhooks,
2264
+ "managed_webhooks",
2265
+ managedWebhookSchema,
2266
+ syncTimestamp
2267
+ );
2268
+ }
2269
+ async backfillSubscriptions(subscriptionIds) {
2270
+ const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
2271
+ "subscriptions",
2272
+ subscriptionIds
2273
+ );
2274
+ await this.fetchMissingEntities(
2275
+ missingSubscriptionIds,
2276
+ (id) => this.stripe.subscriptions.retrieve(id)
2277
+ ).then((subscriptions) => this.upsertSubscriptions(subscriptions));
2278
+ }
2279
+ backfillSubscriptionSchedules = async (subscriptionIds) => {
2280
+ const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
2281
+ "subscription_schedules",
2282
+ subscriptionIds
2283
+ );
2284
+ await this.fetchMissingEntities(
2285
+ missingSubscriptionIds,
2286
+ (id) => this.stripe.subscriptionSchedules.retrieve(id)
2287
+ ).then((subscriptionSchedules) => this.upsertSubscriptionSchedules(subscriptionSchedules));
2288
+ };
2289
+ /**
2290
+ * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
2291
+ */
2292
+ async expandEntity(entities, property, listFn) {
2293
+ if (!this.config.autoExpandLists) return;
2294
+ for (const entity of entities) {
2295
+ if (entity[property]?.has_more) {
2296
+ const allData = [];
2297
+ for await (const fetchedEntity of listFn(entity.id)) {
2298
+ allData.push(fetchedEntity);
2299
+ }
2300
+ entity[property] = {
2301
+ ...entity[property],
2302
+ data: allData,
2303
+ has_more: false
2304
+ };
2305
+ }
2306
+ }
2307
+ }
2308
+ async fetchMissingEntities(ids, fetch) {
2309
+ if (!ids.length) return [];
2310
+ const entities = [];
2311
+ for (const id of ids) {
2312
+ const entity = await fetch(id);
2313
+ entities.push(entity);
2314
+ }
2315
+ return entities;
2316
+ }
2317
+ };
2318
+ function chunkArray(array, chunkSize) {
2319
+ const result = [];
2320
+ for (let i = 0; i < array.length; i += chunkSize) {
2321
+ result.push(array.slice(i, i + chunkSize));
2322
+ }
2323
+ return result;
2324
+ }
2325
+ export {
2326
+ PostgresClient,
2327
+ StripeAutoSync
2328
+ };