@paymentsdb/sync-engine 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
46
46
  // package.json
47
47
  var package_default = {
48
48
  name: "@paymentsdb/sync-engine",
49
- version: "0.0.4",
49
+ version: "0.0.7",
50
50
  private: false,
51
51
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
52
52
  type: "module",
@@ -45444,6 +45444,13 @@ var StripeSync = class {
45444
45444
  listFn: (p) => this.stripe.checkout.sessions.list(p),
45445
45445
  upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
45446
45446
  supportsCreatedFilter: true
45447
+ },
45448
+ _event_catchup: {
45449
+ order: 99,
45450
+ // Always runs last — catches missed webhook events
45451
+ listFn: (_p) => Promise.resolve({ data: [], has_more: false }),
45452
+ upsertFn: async () => [],
45453
+ supportsCreatedFilter: true
45447
45454
  }
45448
45455
  };
45449
45456
  const maxOrder = Math.max(...Object.values(core).map((cfg) => cfg.order));
@@ -46210,7 +46217,8 @@ ${message}`;
46210
46217
  credit_note: "credit_notes",
46211
46218
  early_fraud_warning: "early_fraud_warnings",
46212
46219
  refund: "refunds",
46213
- checkout_sessions: "checkout_sessions"
46220
+ checkout_sessions: "checkout_sessions",
46221
+ _event_catchup: "_event_catchup"
46214
46222
  };
46215
46223
  return mapping[object] || object;
46216
46224
  }
@@ -46221,11 +46229,16 @@ ${message}`;
46221
46229
  */
46222
46230
  async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
46223
46231
  const limit = 100;
46224
- if (object === "payment_method" || object === "tax_id") {
46225
- this.config.logger?.warn(`processNext for ${object} requires customer context`);
46232
+ if (object === "payment_method") {
46233
+ return this.fetchOnePagePaymentMethods(accountId, resourceName, runStartedAt, cursor, pageCursor, params);
46234
+ }
46235
+ if (object === "tax_id") {
46226
46236
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46227
46237
  return { processed: 0, hasMore: false, runStartedAt };
46228
46238
  }
46239
+ if (object === "_event_catchup") {
46240
+ return this.fetchOnePageEventCatchup(accountId, resourceName, runStartedAt, cursor, pageCursor);
46241
+ }
46229
46242
  const config = this.resourceRegistry[object];
46230
46243
  if (!config) {
46231
46244
  throw new Error(`Unsupported object type for processNext: ${object}`);
@@ -46402,6 +46415,437 @@ ${message}`;
46402
46415
  }
46403
46416
  return { processed: entries.length, hasMore, runStartedAt };
46404
46417
  }
46418
+ /**
46419
+ * Fetch payment methods by iterating customers in batches.
46420
+ *
46421
+ * Most customers have 1-2 PMs, so processing one customer per processNext call
46422
+ * would be extremely wasteful (10,000 customers = 10,000 worker round-trips).
46423
+ * Instead, each call fetches a batch of customers (sized by maxConcurrentCustomers,
46424
+ * default 10) and lists+upserts PMs for all of them concurrently.
46425
+ *
46426
+ * Cursor semantics:
46427
+ * - cursor: max customer `created` timestamp — filters to only new customers on subsequent runs
46428
+ * - pageCursor: last processed customer ID — tracks position in customer iteration
46429
+ */
46430
+ async fetchOnePagePaymentMethods(accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
46431
+ const batchSize = this.config.maxConcurrentCustomers ?? 10;
46432
+ try {
46433
+ const customerBatch = await this.findNextCustomerBatchForPmSync(
46434
+ accountId,
46435
+ pageCursor,
46436
+ cursor,
46437
+ batchSize
46438
+ );
46439
+ if (customerBatch.length === 0) {
46440
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46441
+ return { processed: 0, hasMore: false, runStartedAt };
46442
+ }
46443
+ this.config.logger?.info(
46444
+ `processNext: fetching payment_methods for ${customerBatch.length} customers`
46445
+ );
46446
+ let totalProcessed = 0;
46447
+ await Promise.all(
46448
+ customerBatch.map(async (customer) => {
46449
+ const allPms = [];
46450
+ let hasMore = true;
46451
+ let startingAfter;
46452
+ while (hasMore) {
46453
+ const response = await this.stripe.paymentMethods.list({
46454
+ customer: customer.id,
46455
+ limit: 100,
46456
+ ...startingAfter ? { starting_after: startingAfter } : {}
46457
+ });
46458
+ allPms.push(...response.data);
46459
+ hasMore = response.has_more;
46460
+ if (response.data.length > 0) {
46461
+ startingAfter = response.data[response.data.length - 1].id;
46462
+ }
46463
+ }
46464
+ if (allPms.length > 0) {
46465
+ await this.upsertPaymentMethods(allPms, accountId, params?.backfillRelatedEntities);
46466
+ totalProcessed += allPms.length;
46467
+ }
46468
+ })
46469
+ );
46470
+ if (totalProcessed > 0) {
46471
+ await this.postgresClient.incrementObjectProgress(
46472
+ accountId,
46473
+ runStartedAt,
46474
+ resourceName,
46475
+ totalProcessed
46476
+ );
46477
+ }
46478
+ const maxCreated = Math.max(...customerBatch.map((c) => c.created).filter((c) => c != null));
46479
+ if (maxCreated > 0) {
46480
+ await this.postgresClient.updateObjectCursor(
46481
+ accountId,
46482
+ runStartedAt,
46483
+ resourceName,
46484
+ String(maxCreated)
46485
+ );
46486
+ }
46487
+ const lastCustomerId = customerBatch[customerBatch.length - 1].id;
46488
+ const nextBatch = await this.findNextCustomerBatchForPmSync(
46489
+ accountId,
46490
+ lastCustomerId,
46491
+ cursor,
46492
+ 1
46493
+ );
46494
+ if (nextBatch.length > 0) {
46495
+ await this.postgresClient.updateObjectPageCursor(
46496
+ accountId,
46497
+ runStartedAt,
46498
+ resourceName,
46499
+ lastCustomerId
46500
+ );
46501
+ return { processed: totalProcessed, hasMore: true, runStartedAt };
46502
+ }
46503
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46504
+ return { processed: totalProcessed, hasMore: false, runStartedAt };
46505
+ } catch (error) {
46506
+ await this.postgresClient.failObjectSync(
46507
+ accountId,
46508
+ runStartedAt,
46509
+ resourceName,
46510
+ error instanceof Error ? error.message : "Unknown error"
46511
+ );
46512
+ throw error;
46513
+ }
46514
+ }
46515
+ /**
46516
+ * Fetch the next batch of non-deleted customers for PM sync.
46517
+ * Returns customer IDs and created timestamps, ordered by id ASC.
46518
+ */
46519
+ async findNextCustomerBatchForPmSync(accountId, afterCustomerId, cursor, limit) {
46520
+ let query = `SELECT id, created FROM stripe.customers WHERE _account_id = $1 AND COALESCE(deleted, false) <> true`;
46521
+ const params = [accountId];
46522
+ if (afterCustomerId) {
46523
+ params.push(afterCustomerId);
46524
+ query += ` AND id > $${params.length}`;
46525
+ }
46526
+ if (cursor && /^\d+$/.test(cursor)) {
46527
+ params.push(Number.parseInt(cursor, 10));
46528
+ query += ` AND created >= $${params.length}`;
46529
+ }
46530
+ params.push(limit);
46531
+ query += ` ORDER BY id ASC LIMIT $${params.length}`;
46532
+ const result = await this.postgresClient.query(query, params);
46533
+ return result.rows;
46534
+ }
46535
+ /**
46536
+ * Fetch one page of events from the Stripe Events API and reconcile affected entities.
46537
+ *
46538
+ * Instead of replaying events (which can resurrect deleted objects due to newest-first ordering),
46539
+ * we deduplicate by entity and re-fetch current state from Stripe for each affected entity.
46540
+ *
46541
+ * Cursor: event `created` timestamp. On first run, starts from the sync run's startedAt.
46542
+ * On subsequent runs, picks up where the last completed run left off.
46543
+ */
46544
+ async fetchOnePageEventCatchup(accountId, resourceName, runStartedAt, cursor, pageCursor) {
46545
+ try {
46546
+ let createdGte;
46547
+ if (cursor && /^\d+$/.test(cursor)) {
46548
+ createdGte = Number.parseInt(cursor, 10);
46549
+ } else {
46550
+ createdGte = Math.floor(runStartedAt.getTime() / 1e3);
46551
+ }
46552
+ const thirtyDaysAgo = Math.floor(Date.now() / 1e3) - 30 * 24 * 60 * 60;
46553
+ if (createdGte < thirtyDaysAgo) {
46554
+ this.config.logger?.warn(
46555
+ `_event_catchup: cursor ${createdGte} is older than 30 days, clamping to ${thirtyDaysAgo}`
46556
+ );
46557
+ createdGte = thirtyDaysAgo;
46558
+ }
46559
+ const listParams = {
46560
+ limit: 100,
46561
+ created: { gte: createdGte }
46562
+ };
46563
+ if (pageCursor) {
46564
+ listParams.starting_after = pageCursor;
46565
+ }
46566
+ const response = await this.stripe.events.list(listParams);
46567
+ if (response.data.length === 0) {
46568
+ await this.postgresClient.updateObjectCursor(
46569
+ accountId,
46570
+ runStartedAt,
46571
+ resourceName,
46572
+ String(createdGte)
46573
+ );
46574
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46575
+ return { processed: 0, hasMore: false, runStartedAt };
46576
+ }
46577
+ const entityMap = /* @__PURE__ */ new Map();
46578
+ for (const event of response.data) {
46579
+ const obj = event.data.object;
46580
+ if (!obj?.id || !obj?.object) continue;
46581
+ const key = `${obj.object}:${obj.id}`;
46582
+ const existing = entityMap.get(key);
46583
+ if (!existing || event.created > existing.created) {
46584
+ entityMap.set(key, event);
46585
+ }
46586
+ }
46587
+ const hardDeleteEventTypes = /* @__PURE__ */ new Set([
46588
+ "product.deleted",
46589
+ "price.deleted",
46590
+ "plan.deleted",
46591
+ "customer.deleted",
46592
+ "customer.tax_id.deleted"
46593
+ ]);
46594
+ let processed = 0;
46595
+ let skipped = 0;
46596
+ const skipObjectTypes = /* @__PURE__ */ new Set(["tax_id"]);
46597
+ for (const [, event] of entityMap) {
46598
+ const obj = event.data.object;
46599
+ const eventType = event.type;
46600
+ if (skipObjectTypes.has(obj.object)) continue;
46601
+ try {
46602
+ if (!hardDeleteEventTypes.has(eventType)) {
46603
+ const tableName = eventObjectTypeToTable(obj.object);
46604
+ if (tableName) {
46605
+ const localRecord = await this.postgresClient.query(
46606
+ `SELECT "_last_synced_at" FROM stripe."${tableName}"
46607
+ WHERE id = $1 AND "_account_id" = $2 LIMIT 1`,
46608
+ [obj.id, accountId]
46609
+ );
46610
+ if (localRecord.rows.length > 0 && localRecord.rows[0]._last_synced_at != null) {
46611
+ const syncedAt = new Date(localRecord.rows[0]._last_synced_at).getTime() / 1e3;
46612
+ if (syncedAt >= event.created) {
46613
+ skipped++;
46614
+ continue;
46615
+ }
46616
+ }
46617
+ }
46618
+ }
46619
+ if (hardDeleteEventTypes.has(eventType)) {
46620
+ await this.handleEventCatchupDelete(obj.object, obj.id, accountId);
46621
+ } else {
46622
+ await this.handleEventCatchupUpsert(obj.object, obj.id, accountId);
46623
+ }
46624
+ processed++;
46625
+ } catch (err) {
46626
+ const errMsg = err instanceof Error ? err.message : String(err);
46627
+ this.config.logger?.warn(
46628
+ `_event_catchup: failed to process ${obj.object}:${obj.id} (event ${event.id}): ${errMsg}`
46629
+ );
46630
+ }
46631
+ }
46632
+ if (skipped > 0) {
46633
+ this.config.logger?.info(
46634
+ `_event_catchup: skipped ${skipped} entities already up-to-date, processed ${processed}`
46635
+ );
46636
+ }
46637
+ if (processed > 0) {
46638
+ await this.postgresClient.incrementObjectProgress(
46639
+ accountId,
46640
+ runStartedAt,
46641
+ resourceName,
46642
+ processed
46643
+ );
46644
+ }
46645
+ const maxCreated = Math.max(...response.data.map((e) => e.created));
46646
+ await this.postgresClient.updateObjectCursor(
46647
+ accountId,
46648
+ runStartedAt,
46649
+ resourceName,
46650
+ String(maxCreated)
46651
+ );
46652
+ const lastEventId = response.data[response.data.length - 1].id;
46653
+ if (response.has_more) {
46654
+ await this.postgresClient.updateObjectPageCursor(
46655
+ accountId,
46656
+ runStartedAt,
46657
+ resourceName,
46658
+ lastEventId
46659
+ );
46660
+ }
46661
+ if (!response.has_more) {
46662
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46663
+ }
46664
+ return { processed, hasMore: response.has_more, runStartedAt };
46665
+ } catch (error) {
46666
+ await this.postgresClient.failObjectSync(
46667
+ accountId,
46668
+ runStartedAt,
46669
+ resourceName,
46670
+ error instanceof Error ? error.message : "Unknown error"
46671
+ );
46672
+ throw error;
46673
+ }
46674
+ }
46675
+ /**
46676
+ * Handle a delete for an entity discovered via event catch-up.
46677
+ * Maps Stripe object types to the appropriate delete method.
46678
+ */
46679
+ async handleEventCatchupDelete(objectType, entityId, accountId) {
46680
+ switch (objectType) {
46681
+ case "product":
46682
+ await this.deleteProduct(entityId);
46683
+ break;
46684
+ case "price":
46685
+ await this.deletePrice(entityId);
46686
+ break;
46687
+ case "plan":
46688
+ await this.deletePlan(entityId);
46689
+ break;
46690
+ case "customer": {
46691
+ const deletedCustomer = {
46692
+ id: entityId,
46693
+ object: "customer",
46694
+ deleted: true
46695
+ };
46696
+ await this.upsertCustomers([deletedCustomer], accountId);
46697
+ break;
46698
+ }
46699
+ case "tax_id":
46700
+ await this.deleteTaxId(entityId);
46701
+ break;
46702
+ default:
46703
+ this.config.logger?.warn(
46704
+ `_event_catchup: no delete handler for object type "${objectType}", skipping ${entityId}`
46705
+ );
46706
+ }
46707
+ }
46708
+ /**
46709
+ * Handle an upsert for an entity discovered via event catch-up.
46710
+ * Re-fetches the current state from Stripe and upserts it.
46711
+ * If the entity has been deleted (404), falls back to the delete handler.
46712
+ */
46713
+ async handleEventCatchupUpsert(objectType, entityId, accountId) {
46714
+ try {
46715
+ switch (objectType) {
46716
+ case "product": {
46717
+ const product = await this.stripe.products.retrieve(entityId);
46718
+ await this.upsertProducts([product], accountId);
46719
+ break;
46720
+ }
46721
+ case "price": {
46722
+ const price = await this.stripe.prices.retrieve(entityId);
46723
+ await this.upsertPrices([price], accountId);
46724
+ break;
46725
+ }
46726
+ case "plan": {
46727
+ const plan = await this.stripe.plans.retrieve(entityId);
46728
+ await this.upsertPlans([plan], accountId);
46729
+ break;
46730
+ }
46731
+ case "customer": {
46732
+ const customer = await this.stripe.customers.retrieve(entityId);
46733
+ await this.upsertCustomers([customer], accountId);
46734
+ break;
46735
+ }
46736
+ case "subscription": {
46737
+ const sub = await this.stripe.subscriptions.retrieve(entityId);
46738
+ await this.upsertSubscriptions([sub], accountId);
46739
+ break;
46740
+ }
46741
+ case "subscription_schedule": {
46742
+ const schedule = await this.stripe.subscriptionSchedules.retrieve(entityId);
46743
+ await this.upsertSubscriptionSchedules([schedule], accountId);
46744
+ break;
46745
+ }
46746
+ case "invoice": {
46747
+ const invoice = await this.stripe.invoices.retrieve(entityId);
46748
+ await this.upsertInvoices([invoice], accountId);
46749
+ break;
46750
+ }
46751
+ case "charge": {
46752
+ const charge = await this.stripe.charges.retrieve(entityId, {
46753
+ expand: ["balance_transaction"]
46754
+ });
46755
+ if (charge.balance_transaction && typeof charge.balance_transaction === "object") {
46756
+ await this.upsertBalanceTransactions(
46757
+ [charge.balance_transaction],
46758
+ accountId
46759
+ );
46760
+ }
46761
+ await this.upsertCharges([charge], accountId);
46762
+ break;
46763
+ }
46764
+ case "payment_intent": {
46765
+ const pi = await this.stripe.paymentIntents.retrieve(entityId);
46766
+ await this.upsertPaymentIntents([pi], accountId);
46767
+ break;
46768
+ }
46769
+ case "payment_method": {
46770
+ const pm = await this.stripe.paymentMethods.retrieve(entityId);
46771
+ await this.upsertPaymentMethods([pm], accountId);
46772
+ break;
46773
+ }
46774
+ case "setup_intent": {
46775
+ const si = await this.stripe.setupIntents.retrieve(entityId);
46776
+ await this.upsertSetupIntents([si], accountId);
46777
+ break;
46778
+ }
46779
+ case "dispute": {
46780
+ const dispute = await this.stripe.disputes.retrieve(entityId, {
46781
+ expand: ["balance_transactions"]
46782
+ });
46783
+ if (dispute.balance_transactions && Array.isArray(dispute.balance_transactions)) {
46784
+ const expandedBts = dispute.balance_transactions.filter(
46785
+ (bt) => typeof bt === "object"
46786
+ );
46787
+ if (expandedBts.length > 0) {
46788
+ await this.upsertBalanceTransactions(expandedBts, accountId);
46789
+ }
46790
+ }
46791
+ await this.upsertDisputes([dispute], accountId);
46792
+ break;
46793
+ }
46794
+ case "credit_note": {
46795
+ const cn = await this.stripe.creditNotes.retrieve(entityId);
46796
+ await this.upsertCreditNotes([cn], accountId);
46797
+ break;
46798
+ }
46799
+ case "refund": {
46800
+ const refund = await this.stripe.refunds.retrieve(entityId, {
46801
+ expand: ["balance_transaction"]
46802
+ });
46803
+ if (refund.balance_transaction && typeof refund.balance_transaction === "object") {
46804
+ await this.upsertBalanceTransactions(
46805
+ [refund.balance_transaction],
46806
+ accountId
46807
+ );
46808
+ }
46809
+ await this.upsertRefunds([refund], accountId);
46810
+ break;
46811
+ }
46812
+ case "tax_id": {
46813
+ const taxId = await this.stripe.taxIds.retrieve(entityId);
46814
+ await this.upsertTaxIds([taxId], accountId);
46815
+ break;
46816
+ }
46817
+ case "balance_transaction": {
46818
+ const bt = await this.stripe.balanceTransactions.retrieve(entityId);
46819
+ await this.upsertBalanceTransactions([bt], accountId);
46820
+ break;
46821
+ }
46822
+ case "checkout.session": {
46823
+ const session = await this.stripe.checkout.sessions.retrieve(entityId);
46824
+ await this.upsertCheckoutSessions([session], accountId);
46825
+ break;
46826
+ }
46827
+ case "radar.early_fraud_warning": {
46828
+ const efw = await this.stripe.radar.earlyFraudWarnings.retrieve(entityId);
46829
+ await this.upsertEarlyFraudWarning([efw], accountId);
46830
+ break;
46831
+ }
46832
+ default:
46833
+ this.config.logger?.warn(
46834
+ `_event_catchup: no upsert handler for object type "${objectType}", skipping ${entityId}`
46835
+ );
46836
+ }
46837
+ } catch (err) {
46838
+ const isNotFound = (err instanceof import_stripe3.default.errors.StripeInvalidRequestError || err instanceof import_stripe3.default.errors.StripeAPIError) && (err.code === "resource_missing" || err.statusCode === 404);
46839
+ if (isNotFound) {
46840
+ this.config.logger?.info(
46841
+ `_event_catchup: ${objectType}:${entityId} not found on Stripe, treating as deleted`
46842
+ );
46843
+ await this.handleEventCatchupDelete(objectType, entityId, accountId);
46844
+ return;
46845
+ }
46846
+ throw err;
46847
+ }
46848
+ }
46405
46849
  /**
46406
46850
  * Process all pages for all (or specified) object types until complete.
46407
46851
  *
@@ -46475,6 +46919,7 @@ ${message}`;
46475
46919
  };
46476
46920
  }
46477
46921
  applySyncBackfillResult(results, object, result) {
46922
+ if (object === "_event_catchup") return;
46478
46923
  if (this.isSigmaResource(object)) {
46479
46924
  results.sigma = results.sigma ?? {};
46480
46925
  results.sigma[object] = result;
@@ -46510,6 +46955,9 @@ ${message}`;
46510
46955
  case "setup_intent":
46511
46956
  results.setupIntents = result;
46512
46957
  break;
46958
+ case "payment_method":
46959
+ results.paymentMethods = result;
46960
+ break;
46513
46961
  case "payment_intent":
46514
46962
  results.paymentIntents = result;
46515
46963
  break;
@@ -48017,6 +48465,29 @@ function chunkArray(array, chunkSize) {
48017
48465
  }
48018
48466
  return result;
48019
48467
  }
48468
+ function eventObjectTypeToTable(objectType) {
48469
+ const mapping = {
48470
+ product: "products",
48471
+ price: "prices",
48472
+ plan: "plans",
48473
+ customer: "customers",
48474
+ subscription: "subscriptions",
48475
+ subscription_schedule: "subscription_schedules",
48476
+ invoice: "invoices",
48477
+ charge: "charges",
48478
+ balance_transaction: "balance_transactions",
48479
+ payment_intent: "payment_intents",
48480
+ payment_method: "payment_methods",
48481
+ setup_intent: "setup_intents",
48482
+ dispute: "disputes",
48483
+ credit_note: "credit_notes",
48484
+ refund: "refunds",
48485
+ tax_id: "tax_ids",
48486
+ "checkout.session": "checkout_sessions",
48487
+ "radar.early_fraud_warning": "early_fraud_warnings"
48488
+ };
48489
+ return mapping[objectType] ?? null;
48490
+ }
48020
48491
 
48021
48492
  // src/database/migrate.ts
48022
48493
  var import_pg2 = require("pg");
package/dist/index.d.cts CHANGED
@@ -382,7 +382,7 @@ type StripeSyncConfig = {
382
382
  */
383
383
  maxConcurrentCustomers?: number;
384
384
  };
385
- type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'balance_transaction' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions';
385
+ type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'balance_transaction' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions' | '_event_catchup';
386
386
  interface Sync {
387
387
  synced: number;
388
388
  }
@@ -672,6 +672,45 @@ declare class StripeSync {
672
672
  private fetchOnePage;
673
673
  private getSigmaFallbackCursorFromDestination;
674
674
  private fetchOneSigmaPage;
675
+ /**
676
+ * Fetch payment methods by iterating customers in batches.
677
+ *
678
+ * Most customers have 1-2 PMs, so processing one customer per processNext call
679
+ * would be extremely wasteful (10,000 customers = 10,000 worker round-trips).
680
+ * Instead, each call fetches a batch of customers (sized by maxConcurrentCustomers,
681
+ * default 10) and lists+upserts PMs for all of them concurrently.
682
+ *
683
+ * Cursor semantics:
684
+ * - cursor: max customer `created` timestamp — filters to only new customers on subsequent runs
685
+ * - pageCursor: last processed customer ID — tracks position in customer iteration
686
+ */
687
+ private fetchOnePagePaymentMethods;
688
+ /**
689
+ * Fetch the next batch of non-deleted customers for PM sync.
690
+ * Returns customer IDs and created timestamps, ordered by id ASC.
691
+ */
692
+ private findNextCustomerBatchForPmSync;
693
+ /**
694
+ * Fetch one page of events from the Stripe Events API and reconcile affected entities.
695
+ *
696
+ * Instead of replaying events (which can resurrect deleted objects due to newest-first ordering),
697
+ * we deduplicate by entity and re-fetch current state from Stripe for each affected entity.
698
+ *
699
+ * Cursor: event `created` timestamp. On first run, starts from the sync run's startedAt.
700
+ * On subsequent runs, picks up where the last completed run left off.
701
+ */
702
+ private fetchOnePageEventCatchup;
703
+ /**
704
+ * Handle a delete for an entity discovered via event catch-up.
705
+ * Maps Stripe object types to the appropriate delete method.
706
+ */
707
+ private handleEventCatchupDelete;
708
+ /**
709
+ * Handle an upsert for an entity discovered via event catch-up.
710
+ * Re-fetches the current state from Stripe and upserts it.
711
+ * If the entity has been deleted (404), falls back to the delete handler.
712
+ */
713
+ private handleEventCatchupUpsert;
675
714
  /**
676
715
  * Process all pages for all (or specified) object types until complete.
677
716
  *
package/dist/index.d.ts CHANGED
@@ -382,7 +382,7 @@ type StripeSyncConfig = {
382
382
  */
383
383
  maxConcurrentCustomers?: number;
384
384
  };
385
- type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'balance_transaction' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions';
385
+ type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'balance_transaction' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions' | '_event_catchup';
386
386
  interface Sync {
387
387
  synced: number;
388
388
  }
@@ -672,6 +672,45 @@ declare class StripeSync {
672
672
  private fetchOnePage;
673
673
  private getSigmaFallbackCursorFromDestination;
674
674
  private fetchOneSigmaPage;
675
+ /**
676
+ * Fetch payment methods by iterating customers in batches.
677
+ *
678
+ * Most customers have 1-2 PMs, so processing one customer per processNext call
679
+ * would be extremely wasteful (10,000 customers = 10,000 worker round-trips).
680
+ * Instead, each call fetches a batch of customers (sized by maxConcurrentCustomers,
681
+ * default 10) and lists+upserts PMs for all of them concurrently.
682
+ *
683
+ * Cursor semantics:
684
+ * - cursor: max customer `created` timestamp — filters to only new customers on subsequent runs
685
+ * - pageCursor: last processed customer ID — tracks position in customer iteration
686
+ */
687
+ private fetchOnePagePaymentMethods;
688
+ /**
689
+ * Fetch the next batch of non-deleted customers for PM sync.
690
+ * Returns customer IDs and created timestamps, ordered by id ASC.
691
+ */
692
+ private findNextCustomerBatchForPmSync;
693
+ /**
694
+ * Fetch one page of events from the Stripe Events API and reconcile affected entities.
695
+ *
696
+ * Instead of replaying events (which can resurrect deleted objects due to newest-first ordering),
697
+ * we deduplicate by entity and re-fetch current state from Stripe for each affected entity.
698
+ *
699
+ * Cursor: event `created` timestamp. On first run, starts from the sync run's startedAt.
700
+ * On subsequent runs, picks up where the last completed run left off.
701
+ */
702
+ private fetchOnePageEventCatchup;
703
+ /**
704
+ * Handle a delete for an entity discovered via event catch-up.
705
+ * Maps Stripe object types to the appropriate delete method.
706
+ */
707
+ private handleEventCatchupDelete;
708
+ /**
709
+ * Handle an upsert for an entity discovered via event catch-up.
710
+ * Re-fetches the current state from Stripe and upserts it.
711
+ * If the entity has been deleted (404), falls back to the delete handler.
712
+ */
713
+ private handleEventCatchupUpsert;
675
714
  /**
676
715
  * Process all pages for all (or specified) object types until complete.
677
716
  *
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  createStripeWebSocketClient,
6
6
  hashApiKey,
7
7
  runMigrations
8
- } from "./chunk-LB4HG4Q6.js";
9
- import "./chunk-HXSDJSKR.js";
8
+ } from "./chunk-IQ64IUIL.js";
9
+ import "./chunk-7DYBM7H3.js";
10
10
  export {
11
11
  PostgresClient,
12
12
  StripeSync,
@@ -59,7 +59,7 @@ var sigmaWorkerFunctionCode = sigma_data_worker_default;
59
59
  // package.json
60
60
  var package_default = {
61
61
  name: "@paymentsdb/sync-engine",
62
- version: "0.0.4",
62
+ version: "0.0.7",
63
63
  private: false,
64
64
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
65
65
  type: "module",
@@ -10,8 +10,8 @@ import {
10
10
  uninstall,
11
11
  webhookFunctionCode,
12
12
  workerFunctionCode
13
- } from "../chunk-Q3AGYUHN.js";
14
- import "../chunk-HXSDJSKR.js";
13
+ } from "../chunk-OLHHINPH.js";
14
+ import "../chunk-7DYBM7H3.js";
15
15
  export {
16
16
  INSTALLATION_ERROR_SUFFIX,
17
17
  INSTALLATION_INSTALLED_SUFFIX,