stripe-experiment-sync 0.0.4 → 1.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.
package/dist/index.cjs CHANGED
@@ -31,7 +31,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  PostgresClient: () => PostgresClient,
34
- StripeAutoSync: () => StripeAutoSync,
34
+ StripeSync: () => StripeSync,
35
+ VERSION: () => VERSION,
36
+ hashApiKey: () => hashApiKey,
35
37
  runMigrations: () => runMigrations
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
@@ -40,32 +42,164 @@ module.exports = __toCommonJS(index_exports);
40
42
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
41
43
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
42
44
 
45
+ // package.json
46
+ var package_default = {
47
+ name: "stripe-experiment-sync",
48
+ version: "1.0.0",
49
+ private: false,
50
+ description: "Stripe Sync Engine to sync Stripe data to Postgres",
51
+ type: "module",
52
+ main: "./dist/index.cjs",
53
+ exports: {
54
+ ".": {
55
+ import: {
56
+ types: "./dist/index.d.ts",
57
+ default: "./dist/index.js"
58
+ },
59
+ require: {
60
+ types: "./dist/index.d.cts",
61
+ default: "./dist/index.cjs"
62
+ }
63
+ },
64
+ "./pg": {
65
+ import: {
66
+ types: "./dist/pg.d.ts",
67
+ default: "./dist/pg.js"
68
+ },
69
+ require: {
70
+ types: "./dist/pg.d.cts",
71
+ default: "./dist/pg.cjs"
72
+ }
73
+ },
74
+ "./postgres-js": {
75
+ import: {
76
+ types: "./dist/postgres-js.d.ts",
77
+ default: "./dist/postgres-js.js"
78
+ },
79
+ require: {
80
+ types: "./dist/postgres-js.d.cts",
81
+ default: "./dist/postgres-js.cjs"
82
+ }
83
+ }
84
+ },
85
+ scripts: {
86
+ clean: "rimraf dist",
87
+ prebuild: "npm run clean",
88
+ build: "tsup src/index.ts src/pg.ts src/postgres-js.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
89
+ lint: "eslint src --ext .ts",
90
+ test: "vitest"
91
+ },
92
+ files: [
93
+ "dist"
94
+ ],
95
+ dependencies: {
96
+ pg: "^8.16.3",
97
+ "pg-node-migrations": "0.0.8",
98
+ postgres: "^3.4.7",
99
+ ws: "^8.18.0",
100
+ yesql: "^7.0.0"
101
+ },
102
+ peerDependencies: {
103
+ stripe: "> 11"
104
+ },
105
+ devDependencies: {
106
+ "@types/node": "^24.10.1",
107
+ "@types/pg": "^8.15.5",
108
+ "@types/ws": "^8.5.13",
109
+ "@types/yesql": "^4.1.4",
110
+ "@vitest/ui": "^4.0.9",
111
+ stripe: "^20.0.0",
112
+ vitest: "^3.2.4"
113
+ },
114
+ repository: {
115
+ type: "git",
116
+ url: "https://github.com/tx-stripe/stripe-sync-engine.git"
117
+ },
118
+ homepage: "https://github.com/tx-stripe/stripe-sync-engine#readme",
119
+ bugs: {
120
+ url: "https://github.com/tx-stripe/stripe-sync-engine/issues"
121
+ },
122
+ keywords: [
123
+ "stripe",
124
+ "postgres",
125
+ "sync",
126
+ "webhooks",
127
+ "supabase",
128
+ "billing",
129
+ "database",
130
+ "typescript"
131
+ ],
132
+ author: "Supabase <https://supabase.com/>"
133
+ };
134
+
43
135
  // src/stripeSync.ts
44
- var import_stripe = __toESM(require("stripe"), 1);
136
+ var import_stripe2 = __toESM(require("stripe"), 1);
45
137
  var import_yesql2 = require("yesql");
46
138
 
47
139
  // src/database/postgres.ts
48
- var import_pg = __toESM(require("pg"), 1);
49
140
  var import_yesql = require("yesql");
141
+ var ORDERED_STRIPE_TABLES = [
142
+ "subscription_items",
143
+ "subscriptions",
144
+ "subscription_schedules",
145
+ "checkout_session_line_items",
146
+ "checkout_sessions",
147
+ "tax_ids",
148
+ "charges",
149
+ "refunds",
150
+ "credit_notes",
151
+ "disputes",
152
+ "early_fraud_warnings",
153
+ "invoices",
154
+ "payment_intents",
155
+ "payment_methods",
156
+ "setup_intents",
157
+ "prices",
158
+ "plans",
159
+ "products",
160
+ "features",
161
+ "active_entitlements",
162
+ "reviews",
163
+ "_managed_webhooks",
164
+ "customers",
165
+ "_sync_obj_run",
166
+ // Must be deleted before _sync_run (foreign key)
167
+ "_sync_run"
168
+ ];
169
+ var TABLES_WITH_ACCOUNT_ID = /* @__PURE__ */ new Set(["_managed_webhooks"]);
50
170
  var PostgresClient = class {
51
171
  constructor(config) {
52
172
  this.config = config;
53
- this.pool = new import_pg.default.Pool(config.poolConfig);
173
+ this.adapter = config.adapter;
174
+ }
175
+ adapter;
176
+ /**
177
+ * Get the underlying adapter.
178
+ * Useful for accessing adapter-specific features.
179
+ */
180
+ getAdapter() {
181
+ return this.adapter;
182
+ }
183
+ /**
184
+ * Close all database connections.
185
+ */
186
+ async end() {
187
+ await this.adapter.end();
54
188
  }
55
- pool;
56
189
  async delete(table, id) {
57
190
  const prepared = (0, import_yesql.pg)(`
58
- delete from "${this.config.schema}"."${table}"
191
+ delete from "${this.config.schema}"."${table}"
59
192
  where id = :id
60
193
  returning id;
61
194
  `)({ id });
62
195
  const { rows } = await this.query(prepared.text, prepared.values);
63
196
  return rows.length > 0;
64
197
  }
198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
199
  async query(text, params) {
66
- return this.pool.query(text, params);
200
+ return this.adapter.query(text, params);
67
201
  }
68
- async upsertMany(entries, table, tableSchema) {
202
+ async upsertMany(entries, table) {
69
203
  if (!entries.length) return [];
70
204
  const chunkSize = 5;
71
205
  const results = [];
@@ -73,18 +207,22 @@ var PostgresClient = class {
73
207
  const chunk = entries.slice(i, i + chunkSize);
74
208
  const queries = [];
75
209
  chunk.forEach((entry) => {
76
- const cleansed = this.cleanseArrayField(entry);
77
- const upsertSql = this.constructUpsertSql(this.config.schema, table, tableSchema);
78
- const prepared = (0, import_yesql.pg)(upsertSql, {
79
- useNullForMissing: true
80
- })(cleansed);
81
- queries.push(this.pool.query(prepared.text, prepared.values));
210
+ const rawData = JSON.stringify(entry);
211
+ const upsertSql = `
212
+ INSERT INTO "${this.config.schema}"."${table}" ("_raw_data")
213
+ VALUES ($1::jsonb)
214
+ ON CONFLICT (id)
215
+ DO UPDATE SET
216
+ "_raw_data" = EXCLUDED."_raw_data"
217
+ RETURNING *
218
+ `;
219
+ queries.push(this.adapter.query(upsertSql, [rawData]));
82
220
  });
83
221
  results.push(...await Promise.all(queries));
84
222
  }
85
223
  return results.flatMap((it) => it.rows);
86
224
  }
87
- async upsertManyWithTimestampProtection(entries, table, tableSchema, syncTimestamp) {
225
+ async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
88
226
  const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
89
227
  if (!entries.length) return [];
90
228
  const chunkSize = 5;
@@ -93,22 +231,62 @@ var PostgresClient = class {
93
231
  const chunk = entries.slice(i, i + chunkSize);
94
232
  const queries = [];
95
233
  chunk.forEach((entry) => {
96
- const cleansed = this.cleanseArrayField(entry);
97
- cleansed.last_synced_at = timestamp;
98
- const upsertSql = this.constructUpsertWithTimestampProtectionSql(
99
- this.config.schema,
100
- table,
101
- tableSchema
102
- );
103
- const prepared = (0, import_yesql.pg)(upsertSql, {
104
- useNullForMissing: true
105
- })(cleansed);
106
- queries.push(this.pool.query(prepared.text, prepared.values));
234
+ if (table.startsWith("_")) {
235
+ const columns = Object.keys(entry).filter(
236
+ (k) => k !== "last_synced_at" && k !== "account_id"
237
+ );
238
+ const upsertSql = `
239
+ INSERT INTO "${this.config.schema}"."${table}" (
240
+ ${columns.map((c) => `"${c}"`).join(", ")}, "last_synced_at", "account_id"
241
+ )
242
+ VALUES (
243
+ ${columns.map((c) => `:${c}`).join(", ")}, :last_synced_at, :account_id
244
+ )
245
+ ON CONFLICT ("id")
246
+ DO UPDATE SET
247
+ ${columns.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ")},
248
+ "last_synced_at" = :last_synced_at,
249
+ "account_id" = EXCLUDED."account_id"
250
+ WHERE "${table}"."last_synced_at" IS NULL
251
+ OR "${table}"."last_synced_at" < :last_synced_at
252
+ RETURNING *
253
+ `;
254
+ const cleansed = this.cleanseArrayField(entry);
255
+ cleansed.last_synced_at = timestamp;
256
+ cleansed.account_id = accountId;
257
+ const prepared = (0, import_yesql.pg)(upsertSql, { useNullForMissing: true })(cleansed);
258
+ queries.push(this.adapter.query(prepared.text, prepared.values));
259
+ } else {
260
+ const rawData = JSON.stringify(entry);
261
+ const upsertSql = `
262
+ INSERT INTO "${this.config.schema}"."${table}" ("_raw_data", "_last_synced_at", "_account_id")
263
+ VALUES ($1::jsonb, $2, $3)
264
+ ON CONFLICT (id)
265
+ DO UPDATE SET
266
+ "_raw_data" = EXCLUDED."_raw_data",
267
+ "_last_synced_at" = $2,
268
+ "_account_id" = EXCLUDED."_account_id"
269
+ WHERE "${table}"."_last_synced_at" IS NULL
270
+ OR "${table}"."_last_synced_at" < $2
271
+ RETURNING *
272
+ `;
273
+ queries.push(this.adapter.query(upsertSql, [rawData, timestamp, accountId]));
274
+ }
107
275
  });
108
276
  results.push(...await Promise.all(queries));
109
277
  }
110
278
  return results.flatMap((it) => it.rows);
111
279
  }
280
+ cleanseArrayField(obj) {
281
+ const cleansed = { ...obj };
282
+ Object.keys(cleansed).map((k) => {
283
+ const data = cleansed[k];
284
+ if (Array.isArray(data)) {
285
+ cleansed[k] = JSON.stringify(data);
286
+ }
287
+ });
288
+ return cleansed;
289
+ }
112
290
  async findMissingEntries(table, ids) {
113
291
  if (!ids.length) return [];
114
292
  const prepared = (0, import_yesql.pg)(`
@@ -120,1380 +298,1319 @@ var PostgresClient = class {
120
298
  const missingIds = ids.filter((it) => !existingIds.includes(it));
121
299
  return missingIds;
122
300
  }
301
+ // Account management methods
302
+ async upsertAccount(accountData, apiKeyHash) {
303
+ const rawData = JSON.stringify(accountData.raw_data);
304
+ await this.query(
305
+ `INSERT INTO "${this.config.schema}"."accounts" ("_raw_data", "api_key_hashes", "first_synced_at", "_last_synced_at")
306
+ VALUES ($1::jsonb, ARRAY[$2], now(), now())
307
+ ON CONFLICT (id)
308
+ DO UPDATE SET
309
+ "_raw_data" = EXCLUDED."_raw_data",
310
+ "api_key_hashes" = (
311
+ SELECT ARRAY(
312
+ SELECT DISTINCT unnest(
313
+ COALESCE("${this.config.schema}"."accounts"."api_key_hashes", '{}') || ARRAY[$2]
314
+ )
315
+ )
316
+ ),
317
+ "_last_synced_at" = now(),
318
+ "_updated_at" = now()`,
319
+ [rawData, apiKeyHash]
320
+ );
321
+ }
322
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
323
+ async getAllAccounts() {
324
+ const result = await this.query(
325
+ `SELECT _raw_data FROM "${this.config.schema}"."accounts"
326
+ ORDER BY _last_synced_at DESC`
327
+ );
328
+ return result.rows.map((row) => row._raw_data);
329
+ }
330
+ /**
331
+ * Looks up an account ID by API key hash
332
+ * Uses the GIN index on api_key_hashes for fast lookups
333
+ * @param apiKeyHash - SHA-256 hash of the Stripe API key
334
+ * @returns Account ID if found, null otherwise
335
+ */
336
+ async getAccountIdByApiKeyHash(apiKeyHash) {
337
+ const result = await this.query(
338
+ `SELECT id FROM "${this.config.schema}"."accounts"
339
+ WHERE $1 = ANY(api_key_hashes)
340
+ LIMIT 1`,
341
+ [apiKeyHash]
342
+ );
343
+ return result.rows.length > 0 ? result.rows[0].id : null;
344
+ }
345
+ /**
346
+ * Looks up full account data by API key hash
347
+ * @param apiKeyHash - SHA-256 hash of the Stripe API key
348
+ * @returns Account raw data if found, null otherwise
349
+ */
350
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
351
+ async getAccountByApiKeyHash(apiKeyHash) {
352
+ const result = await this.query(
353
+ `SELECT _raw_data FROM "${this.config.schema}"."accounts"
354
+ WHERE $1 = ANY(api_key_hashes)
355
+ LIMIT 1`,
356
+ [apiKeyHash]
357
+ );
358
+ return result.rows.length > 0 ? result.rows[0]._raw_data : null;
359
+ }
360
+ getAccountIdColumn(table) {
361
+ return TABLES_WITH_ACCOUNT_ID.has(table) ? "account_id" : "_account_id";
362
+ }
363
+ async getAccountRecordCounts(accountId) {
364
+ const counts = {};
365
+ for (const table of ORDERED_STRIPE_TABLES) {
366
+ const accountIdColumn = this.getAccountIdColumn(table);
367
+ const result = await this.query(
368
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."${table}"
369
+ WHERE "${accountIdColumn}" = $1`,
370
+ [accountId]
371
+ );
372
+ counts[table] = parseInt(result.rows[0].count);
373
+ }
374
+ return counts;
375
+ }
376
+ async deleteAccountWithCascade(accountId, useTransaction) {
377
+ const deletionCounts = {};
378
+ try {
379
+ if (useTransaction) {
380
+ await this.query("BEGIN");
381
+ }
382
+ for (const table of ORDERED_STRIPE_TABLES) {
383
+ const accountIdColumn = this.getAccountIdColumn(table);
384
+ const result = await this.query(
385
+ `DELETE FROM "${this.config.schema}"."${table}"
386
+ WHERE "${accountIdColumn}" = $1`,
387
+ [accountId]
388
+ );
389
+ deletionCounts[table] = result.rowCount || 0;
390
+ }
391
+ const accountResult = await this.query(
392
+ `DELETE FROM "${this.config.schema}"."accounts"
393
+ WHERE "id" = $1`,
394
+ [accountId]
395
+ );
396
+ deletionCounts["accounts"] = accountResult.rowCount || 0;
397
+ if (useTransaction) {
398
+ await this.query("COMMIT");
399
+ }
400
+ } catch (error) {
401
+ if (useTransaction) {
402
+ await this.query("ROLLBACK");
403
+ }
404
+ throw error;
405
+ }
406
+ return deletionCounts;
407
+ }
123
408
  /**
124
- * Returns an (yesql formatted) upsert function based on the key/vals of an object.
125
- * eg,
126
- * insert into customers ("id", "name")
127
- * values (:id, :name)
128
- * on conflict (id)
129
- * do update set (
130
- * "id" = :id,
131
- * "name" = :name
132
- * )
409
+ * Hash a string to a 32-bit integer for use with PostgreSQL advisory locks.
410
+ * Uses a simple hash algorithm that produces consistent results.
133
411
  */
134
- constructUpsertSql(schema, table, tableSchema, options) {
135
- const { conflict = "id" } = options || {};
136
- const properties = tableSchema.properties;
137
- return `
138
- insert into "${schema}"."${table}" (
139
- ${properties.map((x) => `"${x}"`).join(",")}
140
- )
141
- values (
142
- ${properties.map((x) => `:${x}`).join(",")}
143
- )
144
- on conflict (
145
- ${conflict}
146
- )
147
- do update set
148
- ${properties.map((x) => `"${x}" = :${x}`).join(",")}
149
- ;`;
412
+ hashToInt32(key) {
413
+ let hash = 0;
414
+ for (let i = 0; i < key.length; i++) {
415
+ const char = key.charCodeAt(i);
416
+ hash = (hash << 5) - hash + char;
417
+ hash = hash & hash;
418
+ }
419
+ return hash;
150
420
  }
151
421
  /**
152
- * Returns an (yesql formatted) upsert function with timestamp protection.
422
+ * Acquire a PostgreSQL advisory lock for the given key.
423
+ * This lock is automatically released when the connection is closed or explicitly released.
424
+ * Advisory locks are session-level and will block until the lock is available.
153
425
  *
154
- * The WHERE clause in ON CONFLICT DO UPDATE only applies to the conflicting row
155
- * (the row being updated), not to all rows in the table. PostgreSQL ensures that
156
- * the condition is evaluated only for the specific row that conflicts with the INSERT.
426
+ * @param key - A string key to lock on (will be hashed to an integer)
427
+ */
428
+ async acquireAdvisoryLock(key) {
429
+ const lockId = this.hashToInt32(key);
430
+ await this.query("SELECT pg_advisory_lock($1)", [lockId]);
431
+ }
432
+ /**
433
+ * Release a PostgreSQL advisory lock for the given key.
157
434
  *
435
+ * @param key - The same string key used to acquire the lock
436
+ */
437
+ async releaseAdvisoryLock(key) {
438
+ const lockId = this.hashToInt32(key);
439
+ await this.query("SELECT pg_advisory_unlock($1)", [lockId]);
440
+ }
441
+ /**
442
+ * Execute a function while holding an advisory lock.
443
+ * The lock is automatically released after the function completes (success or error).
158
444
  *
159
- * eg:
160
- * INSERT INTO "stripe"."charges" (
161
- * "id", "amount", "created", "last_synced_at"
162
- * )
163
- * VALUES (
164
- * :id, :amount, :created, :last_synced_at
165
- * )
166
- * ON CONFLICT (id) DO UPDATE SET
167
- * "amount" = EXCLUDED."amount",
168
- * "created" = EXCLUDED."created",
169
- * last_synced_at = :last_synced_at
170
- * WHERE "charges"."last_synced_at" IS NULL
171
- * OR "charges"."last_synced_at" < :last_synced_at;
445
+ * @param key - A string key to lock on (will be hashed to an integer)
446
+ * @param fn - The function to execute while holding the lock
447
+ * @returns The result of the function
172
448
  */
173
- constructUpsertWithTimestampProtectionSql = (schema, table, tableSchema) => {
174
- const conflict = "id";
175
- const properties = tableSchema.properties;
176
- return `
177
- INSERT INTO "${schema}"."${table}" (
178
- ${properties.map((x) => `"${x}"`).join(",")}, "last_synced_at"
179
- )
180
- VALUES (
181
- ${properties.map((x) => `:${x}`).join(",")}, :last_synced_at
182
- )
183
- ON CONFLICT (${conflict}) DO UPDATE SET
184
- ${properties.filter((x) => x !== "last_synced_at").map((x) => `"${x}" = EXCLUDED."${x}"`).join(",")},
185
- last_synced_at = :last_synced_at
186
- WHERE "${table}"."last_synced_at" IS NULL
187
- OR "${table}"."last_synced_at" < :last_synced_at;`;
188
- };
449
+ async withAdvisoryLock(key, fn) {
450
+ const lockId = this.hashToInt32(key);
451
+ return this.adapter.withAdvisoryLock(lockId, fn);
452
+ }
453
+ // =============================================================================
454
+ // Observable Sync System Methods
455
+ // =============================================================================
456
+ // These methods support long-running syncs with full observability.
457
+ // Uses two tables: _sync_run (parent) and _sync_obj_run (children)
458
+ // RunKey = (accountId, runStartedAt) - natural composite key
459
+ /**
460
+ * Cancel stale runs (running but no object updated in 5 minutes).
461
+ * Called before creating a new run to clean up crashed syncs.
462
+ * Only cancels runs that have objects AND none have recent activity.
463
+ * Runs without objects yet (just created) are not considered stale.
464
+ */
465
+ async cancelStaleRuns(accountId) {
466
+ await this.query(
467
+ `UPDATE "${this.config.schema}"."_sync_run" r
468
+ SET status = 'error',
469
+ error_message = 'Auto-cancelled: stale (no update in 5 min)',
470
+ completed_at = now()
471
+ WHERE r."_account_id" = $1
472
+ AND r.status = 'running'
473
+ AND EXISTS (
474
+ SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
475
+ WHERE o."_account_id" = r."_account_id"
476
+ AND o.run_started_at = r.started_at
477
+ )
478
+ AND NOT EXISTS (
479
+ SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
480
+ WHERE o."_account_id" = r."_account_id"
481
+ AND o.run_started_at = r.started_at
482
+ AND o.updated_at >= now() - interval '5 minutes'
483
+ )`,
484
+ [accountId]
485
+ );
486
+ }
189
487
  /**
190
- * For array object field like invoice.custom_fields
191
- * ex: [{"name":"Project name","value":"Test Project"}]
488
+ * Get or create a sync run for this account.
489
+ * Returns existing run if one is active, otherwise creates new one.
490
+ * Auto-cancels stale runs before checking.
192
491
  *
193
- * we need to stringify it first cos passing array object directly will end up with
194
- * {
195
- * invalid input syntax for type json
196
- * detail: 'Expected ":", but found "}".',
197
- * where: 'JSON data, line 1: ...\\":\\"Project name\\",\\"value\\":\\"Test Project\\"}"}',
198
- * }
492
+ * @returns RunKey with isNew flag, or null if constraint violation (race condition)
199
493
  */
200
- cleanseArrayField(obj) {
201
- const cleansed = { ...obj };
202
- Object.keys(cleansed).map((k) => {
203
- const data = cleansed[k];
204
- if (Array.isArray(data)) {
205
- cleansed[k] = JSON.stringify(data);
494
+ async getOrCreateSyncRun(accountId, triggeredBy) {
495
+ await this.cancelStaleRuns(accountId);
496
+ const existing = await this.query(
497
+ `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
498
+ WHERE "_account_id" = $1 AND status = 'running'`,
499
+ [accountId]
500
+ );
501
+ if (existing.rows.length > 0) {
502
+ const row = existing.rows[0];
503
+ return { accountId: row._account_id, runStartedAt: row.started_at, isNew: false };
504
+ }
505
+ try {
506
+ const result = await this.query(
507
+ `INSERT INTO "${this.config.schema}"."_sync_run" ("_account_id", triggered_by, started_at)
508
+ VALUES ($1, $2, date_trunc('milliseconds', now()))
509
+ RETURNING "_account_id", started_at`,
510
+ [accountId, triggeredBy ?? null]
511
+ );
512
+ const row = result.rows[0];
513
+ return { accountId: row._account_id, runStartedAt: row.started_at, isNew: true };
514
+ } catch (error) {
515
+ if (error instanceof Error && "code" in error && error.code === "23P01") {
516
+ return null;
206
517
  }
207
- });
208
- return cleansed;
518
+ throw error;
519
+ }
520
+ }
521
+ /**
522
+ * Get the active sync run for an account (if any).
523
+ */
524
+ async getActiveSyncRun(accountId) {
525
+ const result = await this.query(
526
+ `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
527
+ WHERE "_account_id" = $1 AND status = 'running'`,
528
+ [accountId]
529
+ );
530
+ if (result.rows.length === 0) return null;
531
+ const row = result.rows[0];
532
+ return { accountId: row._account_id, runStartedAt: row.started_at };
533
+ }
534
+ /**
535
+ * Get full sync run details.
536
+ */
537
+ async getSyncRun(accountId, runStartedAt) {
538
+ const result = await this.query(
539
+ `SELECT "_account_id", started_at, status, max_concurrent
540
+ FROM "${this.config.schema}"."_sync_run"
541
+ WHERE "_account_id" = $1 AND started_at = $2`,
542
+ [accountId, runStartedAt]
543
+ );
544
+ if (result.rows.length === 0) return null;
545
+ const row = result.rows[0];
546
+ return {
547
+ accountId: row._account_id,
548
+ runStartedAt: row.started_at,
549
+ status: row.status,
550
+ maxConcurrent: row.max_concurrent
551
+ };
552
+ }
553
+ /**
554
+ * Mark a sync run as complete.
555
+ */
556
+ async completeSyncRun(accountId, runStartedAt) {
557
+ await this.query(
558
+ `UPDATE "${this.config.schema}"."_sync_run"
559
+ SET status = 'complete', completed_at = now()
560
+ WHERE "_account_id" = $1 AND started_at = $2`,
561
+ [accountId, runStartedAt]
562
+ );
563
+ }
564
+ /**
565
+ * Mark a sync run as failed.
566
+ */
567
+ async failSyncRun(accountId, runStartedAt, errorMessage) {
568
+ await this.query(
569
+ `UPDATE "${this.config.schema}"."_sync_run"
570
+ SET status = 'error', error_message = $3, completed_at = now()
571
+ WHERE "_account_id" = $1 AND started_at = $2`,
572
+ [accountId, runStartedAt, errorMessage]
573
+ );
574
+ }
575
+ /**
576
+ * Create object run entries for a sync run.
577
+ * All objects start as 'pending'.
578
+ */
579
+ async createObjectRuns(accountId, runStartedAt, objects) {
580
+ if (objects.length === 0) return;
581
+ const values = objects.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
582
+ await this.query(
583
+ `INSERT INTO "${this.config.schema}"."_sync_obj_run" ("_account_id", run_started_at, object)
584
+ VALUES ${values}
585
+ ON CONFLICT ("_account_id", run_started_at, object) DO NOTHING`,
586
+ [accountId, runStartedAt, ...objects]
587
+ );
588
+ }
589
+ /**
590
+ * Try to start an object sync (respects max_concurrent).
591
+ * Returns true if claimed, false if already running or at concurrency limit.
592
+ *
593
+ * Note: There's a small race window where concurrent calls could result in
594
+ * max_concurrent + 1 objects running. This is acceptable behavior.
595
+ */
596
+ async tryStartObjectSync(accountId, runStartedAt, object) {
597
+ const run = await this.getSyncRun(accountId, runStartedAt);
598
+ if (!run) return false;
599
+ const runningCount = await this.countRunningObjects(accountId, runStartedAt);
600
+ if (runningCount >= run.maxConcurrent) return false;
601
+ const result = await this.query(
602
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
603
+ SET status = 'running', started_at = now(), updated_at = now()
604
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3 AND status = 'pending'
605
+ RETURNING *`,
606
+ [accountId, runStartedAt, object]
607
+ );
608
+ return (result.rowCount ?? 0) > 0;
609
+ }
610
+ /**
611
+ * Get object run details.
612
+ */
613
+ async getObjectRun(accountId, runStartedAt, object) {
614
+ const result = await this.query(
615
+ `SELECT object, status, processed_count, cursor
616
+ FROM "${this.config.schema}"."_sync_obj_run"
617
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
618
+ [accountId, runStartedAt, object]
619
+ );
620
+ if (result.rows.length === 0) return null;
621
+ const row = result.rows[0];
622
+ return {
623
+ object: row.object,
624
+ status: row.status,
625
+ processedCount: row.processed_count,
626
+ cursor: row.cursor
627
+ };
628
+ }
629
+ /**
630
+ * Update progress for an object sync.
631
+ * Also touches updated_at for stale detection.
632
+ */
633
+ async incrementObjectProgress(accountId, runStartedAt, object, count) {
634
+ await this.query(
635
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
636
+ SET processed_count = processed_count + $4, updated_at = now()
637
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
638
+ [accountId, runStartedAt, object, count]
639
+ );
640
+ }
641
+ /**
642
+ * Update the cursor for an object sync.
643
+ * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
644
+ * For numeric cursors (timestamps), uses GREATEST to ensure monotonic increase.
645
+ * For non-numeric cursors, just sets the value directly.
646
+ */
647
+ async updateObjectCursor(accountId, runStartedAt, object, cursor) {
648
+ const isNumeric = cursor !== null && /^\d+$/.test(cursor);
649
+ if (isNumeric) {
650
+ await this.query(
651
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
652
+ SET cursor = GREATEST(COALESCE(cursor::bigint, 0), $4::bigint)::text,
653
+ updated_at = now()
654
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
655
+ [accountId, runStartedAt, object, cursor]
656
+ );
657
+ } else {
658
+ await this.query(
659
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
660
+ SET cursor = $4, updated_at = now()
661
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
662
+ [accountId, runStartedAt, object, cursor]
663
+ );
664
+ }
665
+ }
666
+ /**
667
+ * Get the highest cursor from previous syncs for an object type.
668
+ * This considers completed, error, AND running runs to ensure recovery syncs
669
+ * don't re-process data that was already synced before a crash.
670
+ * A 'running' status with a cursor means the process was killed mid-sync.
671
+ */
672
+ async getLastCompletedCursor(accountId, object) {
673
+ const result = await this.query(
674
+ `SELECT MAX(o.cursor::bigint)::text as cursor
675
+ FROM "${this.config.schema}"."_sync_obj_run" o
676
+ WHERE o."_account_id" = $1
677
+ AND o.object = $2
678
+ AND o.cursor IS NOT NULL`,
679
+ [accountId, object]
680
+ );
681
+ return result.rows[0]?.cursor ?? null;
682
+ }
683
+ /**
684
+ * Delete all sync runs and object runs for an account.
685
+ * Useful for testing or resetting sync state.
686
+ */
687
+ async deleteSyncRuns(accountId) {
688
+ await this.query(
689
+ `DELETE FROM "${this.config.schema}"."_sync_obj_run" WHERE "_account_id" = $1`,
690
+ [accountId]
691
+ );
692
+ await this.query(`DELETE FROM "${this.config.schema}"."_sync_run" WHERE "_account_id" = $1`, [
693
+ accountId
694
+ ]);
695
+ }
696
+ /**
697
+ * Mark an object sync as complete.
698
+ */
699
+ async completeObjectSync(accountId, runStartedAt, object) {
700
+ await this.query(
701
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
702
+ SET status = 'complete', completed_at = now()
703
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
704
+ [accountId, runStartedAt, object]
705
+ );
706
+ }
707
+ /**
708
+ * Mark an object sync as failed.
709
+ */
710
+ async failObjectSync(accountId, runStartedAt, object, errorMessage) {
711
+ await this.query(
712
+ `UPDATE "${this.config.schema}"."_sync_obj_run"
713
+ SET status = 'error', error_message = $4, completed_at = now()
714
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
715
+ [accountId, runStartedAt, object, errorMessage]
716
+ );
717
+ }
718
+ /**
719
+ * Count running objects in a run.
720
+ */
721
+ async countRunningObjects(accountId, runStartedAt) {
722
+ const result = await this.query(
723
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
724
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'running'`,
725
+ [accountId, runStartedAt]
726
+ );
727
+ return parseInt(result.rows[0].count);
728
+ }
729
+ /**
730
+ * Get the next pending object to process.
731
+ * Returns null if no pending objects or at concurrency limit.
732
+ */
733
+ async getNextPendingObject(accountId, runStartedAt) {
734
+ const run = await this.getSyncRun(accountId, runStartedAt);
735
+ if (!run) return null;
736
+ const runningCount = await this.countRunningObjects(accountId, runStartedAt);
737
+ if (runningCount >= run.maxConcurrent) return null;
738
+ const result = await this.query(
739
+ `SELECT object FROM "${this.config.schema}"."_sync_obj_run"
740
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'pending'
741
+ ORDER BY object
742
+ LIMIT 1`,
743
+ [accountId, runStartedAt]
744
+ );
745
+ return result.rows.length > 0 ? result.rows[0].object : null;
746
+ }
747
+ /**
748
+ * Check if all objects in a run are complete (or error).
749
+ */
750
+ async areAllObjectsComplete(accountId, runStartedAt) {
751
+ const result = await this.query(
752
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
753
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND status IN ('pending', 'running')`,
754
+ [accountId, runStartedAt]
755
+ );
756
+ return parseInt(result.rows[0].count) === 0;
209
757
  }
210
758
  };
211
759
 
212
- // src/schemas/charge.ts
213
- var chargeSchema = {
214
- properties: [
215
- "id",
216
- "object",
217
- "paid",
218
- "order",
219
- "amount",
220
- "review",
221
- "source",
222
- "status",
223
- "created",
224
- "dispute",
225
- "invoice",
226
- "outcome",
227
- "refunds",
228
- "captured",
229
- "currency",
230
- "customer",
231
- "livemode",
232
- "metadata",
233
- "refunded",
234
- "shipping",
235
- "application",
236
- "description",
237
- "destination",
238
- "failure_code",
239
- "on_behalf_of",
240
- "fraud_details",
241
- "receipt_email",
242
- "payment_intent",
243
- "receipt_number",
244
- "transfer_group",
245
- "amount_refunded",
246
- "application_fee",
247
- "failure_message",
248
- "source_transfer",
249
- "balance_transaction",
250
- "statement_descriptor",
251
- "payment_method_details"
252
- ]
253
- };
254
-
255
- // src/schemas/checkout_sessions.ts
256
- var checkoutSessionSchema = {
760
+ // src/schemas/managed_webhook.ts
761
+ var managedWebhookSchema = {
257
762
  properties: [
258
763
  "id",
259
764
  "object",
260
- "adaptive_pricing",
261
- "after_expiration",
262
- "allow_promotion_codes",
263
- "amount_subtotal",
264
- "amount_total",
265
- "automatic_tax",
266
- "billing_address_collection",
267
- "cancel_url",
268
- "client_reference_id",
269
- "client_secret",
270
- "collected_information",
271
- "consent",
272
- "consent_collection",
273
- "created",
274
- "currency",
275
- "currency_conversion",
276
- "custom_fields",
277
- "custom_text",
278
- "customer",
279
- "customer_creation",
280
- "customer_details",
281
- "customer_email",
282
- "discounts",
283
- "expires_at",
284
- "invoice",
285
- "invoice_creation",
286
- "livemode",
287
- "locale",
288
- "metadata",
289
- "mode",
290
- "optional_items",
291
- "payment_intent",
292
- "payment_link",
293
- "payment_method_collection",
294
- "payment_method_configuration_details",
295
- "payment_method_options",
296
- "payment_method_types",
297
- "payment_status",
298
- "permissions",
299
- "phone_number_collection",
300
- "presentment_details",
301
- "recovered_from",
302
- "redirect_on_completion",
303
- "return_url",
304
- "saved_payment_method_options",
305
- "setup_intent",
306
- "shipping_address_collection",
307
- "shipping_cost",
308
- "shipping_details",
309
- "shipping_options",
310
- "status",
311
- "submit_type",
312
- "subscription",
313
- "success_url",
314
- "tax_id_collection",
315
- "total_details",
316
- "ui_mode",
317
765
  "url",
318
- "wallet_options"
319
- ]
320
- };
321
-
322
- // src/schemas/checkout_session_line_items.ts
323
- var checkoutSessionLineItemSchema = {
324
- properties: [
325
- "id",
326
- "object",
327
- "amount_discount",
328
- "amount_subtotal",
329
- "amount_tax",
330
- "amount_total",
331
- "currency",
766
+ "enabled_events",
332
767
  "description",
333
- "price",
334
- "quantity",
335
- "checkout_session"
336
- ]
337
- };
338
-
339
- // src/schemas/credit_note.ts
340
- var creditNoteSchema = {
341
- properties: [
342
- "id",
343
- "object",
344
- "amount",
345
- "amount_shipping",
346
- "created",
347
- "currency",
348
- "customer",
349
- "customer_balance_transaction",
350
- "discount_amount",
351
- "discount_amounts",
352
- "invoice",
353
- "lines",
768
+ "enabled",
354
769
  "livemode",
355
- "memo",
356
770
  "metadata",
357
- "number",
358
- "out_of_band_amount",
359
- "pdf",
360
- "reason",
361
- "refund",
362
- "shipping_cost",
771
+ "secret",
363
772
  "status",
364
- "subtotal",
365
- "subtotal_excluding_tax",
366
- "tax_amounts",
367
- "total",
368
- "total_excluding_tax",
369
- "type",
370
- "voided_at"
371
- ]
372
- };
373
-
374
- // src/schemas/customer.ts
375
- var customerSchema = {
376
- properties: [
377
- "id",
378
- "object",
379
- "address",
380
- "description",
381
- "email",
382
- "metadata",
383
- "name",
384
- "phone",
385
- "shipping",
386
- "balance",
773
+ "api_version",
387
774
  "created",
388
- "currency",
389
- "default_source",
390
- "delinquent",
391
- "discount",
392
- "invoice_prefix",
393
- "invoice_settings",
394
- "livemode",
395
- "next_invoice_sequence",
396
- "preferred_locales",
397
- "tax_exempt"
775
+ "account_id"
398
776
  ]
399
777
  };
400
- var customerDeletedSchema = {
401
- properties: ["id", "object", "deleted"]
402
- };
403
778
 
404
- // src/schemas/dispute.ts
405
- var disputeSchema = {
406
- properties: [
407
- "id",
408
- "object",
409
- "amount",
410
- "charge",
411
- "created",
412
- "currency",
413
- "balance_transactions",
414
- "evidence",
415
- "evidence_details",
416
- "is_charge_refundable",
417
- "livemode",
418
- "metadata",
419
- "payment_intent",
420
- "reason",
421
- "status"
422
- ]
779
+ // src/utils/retry.ts
780
+ var import_stripe = __toESM(require("stripe"), 1);
781
+ var DEFAULT_RETRY_CONFIG = {
782
+ maxRetries: 5,
783
+ initialDelayMs: 1e3,
784
+ // 1 second
785
+ maxDelayMs: 6e4,
786
+ // 60 seconds
787
+ jitterMs: 500
788
+ // randomization to prevent thundering herd
423
789
  };
790
+ function isRetryableError(error) {
791
+ if (error instanceof import_stripe.default.errors.StripeRateLimitError) {
792
+ return true;
793
+ }
794
+ if (error instanceof import_stripe.default.errors.StripeAPIError) {
795
+ const statusCode = error.statusCode;
796
+ if (statusCode && [500, 502, 503, 504, 424].includes(statusCode)) {
797
+ return true;
798
+ }
799
+ }
800
+ if (error instanceof import_stripe.default.errors.StripeConnectionError) {
801
+ return true;
802
+ }
803
+ return false;
804
+ }
805
+ function getRetryAfterMs(error) {
806
+ if (!(error instanceof import_stripe.default.errors.StripeRateLimitError)) {
807
+ return null;
808
+ }
809
+ const retryAfterHeader = error.headers?.["retry-after"];
810
+ if (!retryAfterHeader) {
811
+ return null;
812
+ }
813
+ const retryAfterSeconds = Number(retryAfterHeader);
814
+ if (isNaN(retryAfterSeconds) || retryAfterSeconds <= 0) {
815
+ return null;
816
+ }
817
+ return retryAfterSeconds * 1e3;
818
+ }
819
+ function calculateDelay(attempt, config, retryAfterMs) {
820
+ if (retryAfterMs !== null && retryAfterMs !== void 0) {
821
+ const jitter2 = Math.random() * config.jitterMs;
822
+ return retryAfterMs + jitter2;
823
+ }
824
+ const exponentialDelay = Math.min(config.initialDelayMs * Math.pow(2, attempt), config.maxDelayMs);
825
+ const jitter = Math.random() * config.jitterMs;
826
+ return exponentialDelay + jitter;
827
+ }
828
+ function sleep(ms) {
829
+ return new Promise((resolve) => setTimeout(resolve, ms));
830
+ }
831
+ function getErrorType(error) {
832
+ if (error instanceof import_stripe.default.errors.StripeRateLimitError) {
833
+ return "rate_limit";
834
+ }
835
+ if (error instanceof import_stripe.default.errors.StripeAPIError) {
836
+ return `api_error_${error.statusCode}`;
837
+ }
838
+ if (error instanceof import_stripe.default.errors.StripeConnectionError) {
839
+ return "connection_error";
840
+ }
841
+ return "unknown";
842
+ }
843
+ async function withRetry(fn, config = {}, logger) {
844
+ const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
845
+ let lastError;
846
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
847
+ try {
848
+ return await fn();
849
+ } catch (error) {
850
+ lastError = error;
851
+ if (!isRetryableError(error)) {
852
+ throw error;
853
+ }
854
+ if (attempt >= retryConfig.maxRetries) {
855
+ logger?.error(
856
+ {
857
+ error: error instanceof Error ? error.message : String(error),
858
+ errorType: getErrorType(error),
859
+ attempt: attempt + 1,
860
+ maxRetries: retryConfig.maxRetries
861
+ },
862
+ "Max retries exhausted for Stripe error"
863
+ );
864
+ throw error;
865
+ }
866
+ const retryAfterMs = getRetryAfterMs(error);
867
+ const delay = calculateDelay(attempt, retryConfig, retryAfterMs);
868
+ logger?.warn(
869
+ {
870
+ error: error instanceof Error ? error.message : String(error),
871
+ errorType: getErrorType(error),
872
+ attempt: attempt + 1,
873
+ maxRetries: retryConfig.maxRetries,
874
+ delayMs: Math.round(delay),
875
+ retryAfterMs: retryAfterMs ?? void 0,
876
+ nextAttempt: attempt + 2
877
+ },
878
+ "Transient Stripe error, retrying after delay"
879
+ );
880
+ await sleep(delay);
881
+ }
882
+ }
883
+ throw lastError;
884
+ }
424
885
 
425
- // src/schemas/invoice.ts
426
- var invoiceSchema = {
427
- properties: [
428
- "id",
429
- "object",
430
- "auto_advance",
431
- "collection_method",
432
- "currency",
433
- "description",
434
- "hosted_invoice_url",
435
- "lines",
436
- "metadata",
437
- "period_end",
438
- "period_start",
439
- "status",
440
- "total",
441
- "account_country",
442
- "account_name",
443
- "account_tax_ids",
444
- "amount_due",
445
- "amount_paid",
446
- "amount_remaining",
447
- "application_fee_amount",
448
- "attempt_count",
449
- "attempted",
450
- "billing_reason",
451
- "created",
452
- "custom_fields",
453
- "customer_address",
454
- "customer_email",
455
- "customer_name",
456
- "customer_phone",
457
- "customer_shipping",
458
- "customer_tax_exempt",
459
- "customer_tax_ids",
460
- "default_tax_rates",
461
- "discount",
462
- "discounts",
463
- "due_date",
464
- "ending_balance",
465
- "footer",
466
- "invoice_pdf",
467
- "last_finalization_error",
468
- "livemode",
469
- "next_payment_attempt",
470
- "number",
471
- "paid",
472
- "payment_settings",
473
- "post_payment_credit_notes_amount",
474
- "pre_payment_credit_notes_amount",
475
- "receipt_number",
476
- "starting_balance",
477
- "statement_descriptor",
478
- "status_transitions",
479
- "subtotal",
480
- "tax",
481
- "total_discount_amounts",
482
- "total_tax_amounts",
483
- "transfer_data",
484
- "webhooks_delivered_at",
485
- "customer",
486
- "subscription",
487
- "payment_intent",
488
- "default_payment_method",
489
- "default_source",
490
- "on_behalf_of",
491
- "charge"
492
- ]
493
- };
886
+ // src/utils/stripeClientWrapper.ts
887
+ function createRetryableStripeClient(stripe, retryConfig = {}, logger) {
888
+ return new Proxy(stripe, {
889
+ get(target, prop, receiver) {
890
+ const original = Reflect.get(target, prop, receiver);
891
+ if (original && typeof original === "object" && !isPromise(original)) {
892
+ return wrapResource(original, retryConfig, logger);
893
+ }
894
+ return original;
895
+ }
896
+ });
897
+ }
898
+ function wrapResource(resource, retryConfig, logger) {
899
+ return new Proxy(resource, {
900
+ get(target, prop, receiver) {
901
+ const original = Reflect.get(target, prop, receiver);
902
+ if (typeof original === "function") {
903
+ return function(...args) {
904
+ const result = original.apply(target, args);
905
+ if (result && typeof result === "object" && Symbol.asyncIterator in result) {
906
+ return result;
907
+ }
908
+ if (isPromise(result)) {
909
+ return withRetry(() => Promise.resolve(result), retryConfig, logger);
910
+ }
911
+ return result;
912
+ };
913
+ }
914
+ if (original && typeof original === "object" && !isPromise(original)) {
915
+ return wrapResource(original, retryConfig, logger);
916
+ }
917
+ return original;
918
+ }
919
+ });
920
+ }
921
+ function isPromise(value) {
922
+ return value !== null && typeof value === "object" && typeof value.then === "function";
923
+ }
494
924
 
495
- // src/schemas/plan.ts
496
- var planSchema = {
497
- properties: [
498
- "id",
499
- "object",
500
- "active",
501
- "amount",
502
- "created",
503
- "product",
504
- "currency",
505
- "interval",
506
- "livemode",
507
- "metadata",
508
- "nickname",
509
- "tiers_mode",
510
- "usage_type",
511
- "billing_scheme",
512
- "interval_count",
513
- "aggregate_usage",
514
- "transform_usage",
515
- "trial_period_days"
516
- ]
517
- };
518
-
519
- // src/schemas/price.ts
520
- var priceSchema = {
521
- properties: [
522
- "id",
523
- "object",
524
- "active",
525
- "currency",
526
- "metadata",
527
- "nickname",
528
- "recurring",
529
- "type",
530
- "unit_amount",
531
- "billing_scheme",
532
- "created",
533
- "livemode",
534
- "lookup_key",
535
- "tiers_mode",
536
- "transform_quantity",
537
- "unit_amount_decimal",
538
- "product"
539
- ]
540
- };
541
-
542
- // src/schemas/product.ts
543
- var productSchema = {
544
- properties: [
545
- "id",
546
- "object",
547
- "active",
548
- "default_price",
549
- "description",
550
- "metadata",
551
- "name",
552
- "created",
553
- "images",
554
- "marketing_features",
555
- "livemode",
556
- "package_dimensions",
557
- "shippable",
558
- "statement_descriptor",
559
- "unit_label",
560
- "updated",
561
- "url"
562
- ]
563
- };
564
-
565
- // src/schemas/payment_intent.ts
566
- var paymentIntentSchema = {
567
- properties: [
568
- "id",
569
- "object",
570
- "amount",
571
- "amount_capturable",
572
- "amount_details",
573
- "amount_received",
574
- "application",
575
- "application_fee_amount",
576
- "automatic_payment_methods",
577
- "canceled_at",
578
- "cancellation_reason",
579
- "capture_method",
580
- "client_secret",
581
- "confirmation_method",
582
- "created",
583
- "currency",
584
- "customer",
585
- "description",
586
- "invoice",
587
- "last_payment_error",
588
- "livemode",
589
- "metadata",
590
- "next_action",
591
- "on_behalf_of",
592
- "payment_method",
593
- "payment_method_options",
594
- "payment_method_types",
595
- "processing",
596
- "receipt_email",
597
- "review",
598
- "setup_future_usage",
599
- "shipping",
600
- "statement_descriptor",
601
- "statement_descriptor_suffix",
602
- "status",
603
- "transfer_data",
604
- "transfer_group"
605
- ]
606
- };
607
-
608
- // src/schemas/payment_methods.ts
609
- var paymentMethodsSchema = {
610
- properties: [
611
- "id",
612
- "object",
613
- "created",
614
- "customer",
615
- "type",
616
- "billing_details",
617
- "metadata",
618
- "card"
619
- ]
620
- };
621
-
622
- // src/schemas/setup_intents.ts
623
- var setupIntentsSchema = {
624
- properties: [
625
- "id",
626
- "object",
627
- "created",
628
- "customer",
629
- "description",
630
- "payment_method",
631
- "status",
632
- "usage",
633
- "cancellation_reason",
634
- "latest_attempt",
635
- "mandate",
636
- "single_use_mandate",
637
- "on_behalf_of"
638
- ]
639
- };
640
-
641
- // src/schemas/tax_id.ts
642
- var taxIdSchema = {
643
- properties: [
644
- "id",
645
- "country",
646
- "customer",
647
- "type",
648
- "value",
649
- "object",
650
- "created",
651
- "livemode",
652
- "owner"
653
- ]
654
- };
655
-
656
- // src/schemas/subscription_item.ts
657
- var subscriptionItemSchema = {
658
- properties: [
659
- "id",
660
- "object",
661
- "billing_thresholds",
662
- "created",
663
- "deleted",
664
- "metadata",
665
- "quantity",
666
- "price",
667
- "subscription",
668
- "tax_rates",
669
- "current_period_end",
670
- "current_period_start"
671
- ]
672
- };
673
-
674
- // src/schemas/subscription_schedules.ts
675
- var subscriptionScheduleSchema = {
676
- properties: [
677
- "id",
678
- "object",
679
- "application",
680
- "canceled_at",
681
- "completed_at",
682
- "created",
683
- "current_phase",
684
- "customer",
685
- "default_settings",
686
- "end_behavior",
687
- "livemode",
688
- "metadata",
689
- "phases",
690
- "released_at",
691
- "released_subscription",
692
- "status",
693
- "subscription",
694
- "test_clock"
695
- ]
696
- };
697
-
698
- // src/schemas/subscription.ts
699
- var subscriptionSchema = {
700
- properties: [
701
- "id",
702
- "object",
703
- "cancel_at_period_end",
704
- "current_period_end",
705
- "current_period_start",
706
- "default_payment_method",
707
- "items",
708
- "metadata",
709
- "pending_setup_intent",
710
- "pending_update",
711
- "status",
712
- "application_fee_percent",
713
- "billing_cycle_anchor",
714
- "billing_thresholds",
715
- "cancel_at",
716
- "canceled_at",
717
- "collection_method",
718
- "created",
719
- "days_until_due",
720
- "default_source",
721
- "default_tax_rates",
722
- "discount",
723
- "ended_at",
724
- "livemode",
725
- "next_pending_invoice_item_invoice",
726
- "pause_collection",
727
- "pending_invoice_item_interval",
728
- "start_date",
729
- "transfer_data",
730
- "trial_end",
731
- "trial_start",
732
- "schedule",
733
- "customer",
734
- "latest_invoice",
735
- "plan"
736
- ]
737
- };
738
-
739
- // src/schemas/early_fraud_warning.ts
740
- var earlyFraudWarningSchema = {
741
- properties: [
742
- "id",
743
- "object",
744
- "actionable",
745
- "charge",
746
- "created",
747
- "fraud_type",
748
- "livemode",
749
- "payment_intent"
750
- ]
751
- };
752
-
753
- // src/schemas/review.ts
754
- var reviewSchema = {
755
- properties: [
756
- "id",
757
- "object",
758
- "billing_zip",
759
- "created",
760
- "charge",
761
- "closed_reason",
762
- "livemode",
763
- "ip_address",
764
- "ip_address_location",
765
- "open",
766
- "opened_reason",
767
- "payment_intent",
768
- "reason",
769
- "session"
770
- ]
771
- };
772
-
773
- // src/schemas/refund.ts
774
- var refundSchema = {
775
- properties: [
776
- "id",
777
- "object",
778
- "amount",
779
- "balance_transaction",
780
- "charge",
781
- "created",
782
- "currency",
783
- "destination_details",
784
- "metadata",
785
- "payment_intent",
786
- "reason",
787
- "receipt_number",
788
- "source_transfer_reversal",
789
- "status",
790
- "transfer_reversal"
791
- ]
792
- };
793
-
794
- // src/schemas/active_entitlement.ts
795
- var activeEntitlementSchema = {
796
- properties: ["id", "object", "feature", "lookup_key", "livemode", "customer"]
797
- };
798
-
799
- // src/schemas/feature.ts
800
- var featureSchema = {
801
- properties: ["id", "object", "livemode", "name", "lookup_key", "active", "metadata"]
802
- };
803
-
804
- // src/schemas/managed_webhook.ts
805
- var managedWebhookSchema = {
806
- properties: [
807
- "id",
808
- "object",
809
- "uuid",
810
- "url",
811
- "enabled_events",
812
- "description",
813
- "enabled",
814
- "livemode",
815
- "metadata",
816
- "secret",
817
- "status",
818
- "api_version",
819
- "created"
820
- ]
821
- };
822
-
823
- // src/stripeSync.ts
824
- var import_node_crypto = require("crypto");
825
-
826
- // src/database/migrate.ts
827
- var import_pg2 = require("pg");
828
- var import_pg_node_migrations = require("pg-node-migrations");
829
- var import_node_fs = __toESM(require("fs"), 1);
830
- var import_node_path = __toESM(require("path"), 1);
831
- var import_node_url = require("url");
832
- var __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
833
- var __dirname = import_node_path.default.dirname(__filename2);
834
- async function connectAndMigrate(client, migrationsDirectory, config, logOnError = false) {
835
- if (!import_node_fs.default.existsSync(migrationsDirectory)) {
836
- config.logger?.info(`Migrations directory ${migrationsDirectory} not found, skipping`);
837
- return;
838
- }
839
- const optionalConfig = {
840
- schemaName: config.schema,
841
- tableName: "migrations"
842
- };
843
- try {
844
- await (0, import_pg_node_migrations.migrate)({ client }, migrationsDirectory, optionalConfig);
845
- } catch (error) {
846
- if (logOnError && error instanceof Error) {
847
- config.logger?.error(error, "Migration error:");
848
- } else {
849
- throw error;
850
- }
851
- }
852
- }
853
- async function runMigrations(config) {
854
- const client = new import_pg2.Client({
855
- connectionString: config.databaseUrl,
856
- ssl: config.ssl,
857
- connectionTimeoutMillis: 1e4
858
- });
859
- try {
860
- await client.connect();
861
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${config.schema};`);
862
- config.logger?.info("Running migrations");
863
- await connectAndMigrate(client, import_node_path.default.resolve(__dirname, "./migrations"), config);
864
- } catch (err) {
865
- config.logger?.error(err, "Error running migrations");
866
- throw err;
867
- } finally {
868
- await client.end();
869
- config.logger?.info("Finished migrations");
870
- }
925
+ // src/utils/hashApiKey.ts
926
+ var import_crypto = require("crypto");
927
+ function hashApiKey(apiKey) {
928
+ return (0, import_crypto.createHash)("sha256").update(apiKey).digest("hex");
871
929
  }
872
930
 
873
931
  // src/stripeSync.ts
874
- var import_express = __toESM(require("express"), 1);
875
932
  function getUniqueIds(entries, key) {
876
933
  const set = new Set(
877
934
  entries.map((subscription) => subscription?.[key]?.toString()).filter((it) => Boolean(it))
878
935
  );
879
936
  return Array.from(set);
880
937
  }
881
- var DEFAULT_SCHEMA = "stripe";
882
- var StripeAutoSync = class {
883
- options;
884
- webhookId = null;
885
- webhookUuid = null;
886
- stripeSync = null;
887
- constructor(options) {
888
- this.options = {
889
- ...options,
890
- // Apply defaults for undefined values
891
- schema: options.schema || "stripe",
892
- webhookPath: options.webhookPath || "/stripe-webhooks",
893
- stripeApiVersion: options.stripeApiVersion || "2020-08-27",
894
- autoExpandLists: options.autoExpandLists !== void 0 ? options.autoExpandLists : false,
895
- backfillRelatedEntities: options.backfillRelatedEntities !== void 0 ? options.backfillRelatedEntities : true,
896
- keepWebhooksOnShutdown: options.keepWebhooksOnShutdown !== void 0 ? options.keepWebhooksOnShutdown : true
897
- };
938
+ var StripeSync = class {
939
+ constructor(config) {
940
+ this.config = config;
941
+ const baseStripe = new import_stripe2.default(config.stripeSecretKey, {
942
+ // https://github.com/stripe/stripe-node#configuration
943
+ // @ts-ignore
944
+ apiVersion: config.stripeApiVersion,
945
+ appInfo: {
946
+ name: "Stripe Postgres Sync"
947
+ }
948
+ });
949
+ this.stripe = createRetryableStripeClient(baseStripe, {}, config.logger);
950
+ this.config.logger = config.logger ?? console;
951
+ this.config.logger?.info(
952
+ { autoExpandLists: config.autoExpandLists, stripeApiVersion: config.stripeApiVersion },
953
+ "StripeSync initialized"
954
+ );
955
+ this.postgresClient = new PostgresClient({
956
+ schema: "stripe",
957
+ adapter: config.adapter
958
+ });
898
959
  }
960
+ stripe;
961
+ postgresClient;
899
962
  /**
900
- * Starts the Stripe Sync infrastructure and mounts webhook handler:
901
- * 1. Runs database migrations
902
- * 2. Creates StripeSync instance
903
- * 3. Creates managed webhook endpoint
904
- * 4. Mounts webhook handler on provided Express app
905
- * 5. Applies body parsing middleware (automatically skips webhook routes)
906
- *
907
- * @param app - Express app to mount webhook handler on
908
- * @returns Information about the running instance
963
+ * Get the Stripe account ID. Delegates to getCurrentAccount() for the actual lookup.
964
+ */
965
+ async getAccountId(objectAccountId) {
966
+ const account = await this.getCurrentAccount(objectAccountId);
967
+ if (!account) {
968
+ throw new Error("Failed to retrieve Stripe account. Please ensure API key is valid.");
969
+ }
970
+ return account.id;
971
+ }
972
+ /**
973
+ * Upsert Stripe account information to the database
974
+ * @param account - Stripe account object
975
+ * @param apiKeyHash - SHA-256 hash of API key to store for fast lookups
909
976
  */
910
- async start(app) {
977
+ async upsertAccount(account, apiKeyHash) {
911
978
  try {
912
- try {
913
- await runMigrations({
914
- databaseUrl: this.options.databaseUrl,
915
- schema: this.options.schema
916
- });
917
- } catch (migrationError) {
918
- console.warn("Migration failed, dropping schema and retrying...");
919
- console.warn("Migration error:", migrationError instanceof Error ? migrationError.message : String(migrationError));
920
- const { Client: Client2 } = await import("pg");
921
- const client = new Client2({ connectionString: this.options.databaseUrl });
922
- try {
923
- await client.connect();
924
- await client.query(`DROP SCHEMA IF EXISTS "${this.options.schema}" CASCADE`);
925
- console.log(`\u2713 Dropped schema: ${this.options.schema}`);
926
- } finally {
927
- await client.end();
928
- }
929
- console.log("Retrying migrations...");
930
- await runMigrations({
931
- databaseUrl: this.options.databaseUrl,
932
- schema: this.options.schema
933
- });
934
- console.log("\u2713 Migrations completed successfully after retry");
935
- }
936
- const poolConfig = {
937
- max: 10,
938
- connectionString: this.options.databaseUrl,
939
- keepAlive: true
940
- };
941
- this.stripeSync = new StripeSync({
942
- databaseUrl: this.options.databaseUrl,
943
- schema: this.options.schema,
944
- stripeSecretKey: this.options.stripeApiKey,
945
- stripeApiVersion: this.options.stripeApiVersion,
946
- autoExpandLists: this.options.autoExpandLists,
947
- backfillRelatedEntities: this.options.backfillRelatedEntities,
948
- poolConfig
949
- });
950
- const baseUrl = this.options.baseUrl();
951
- const { webhook, uuid } = await this.stripeSync.findOrCreateManagedWebhook(
952
- `${baseUrl}${this.options.webhookPath}`,
979
+ await this.postgresClient.upsertAccount(
953
980
  {
954
- enabled_events: ["*"],
955
- // Subscribe to all events
956
- description: "stripe-sync-cli development webhook"
957
- }
981
+ id: account.id,
982
+ raw_data: account
983
+ },
984
+ apiKeyHash
958
985
  );
959
- this.webhookId = webhook.id;
960
- this.webhookUuid = uuid;
961
- app.use(this.getBodyParserMiddleware());
962
- this.mountWebhook(app);
963
- console.log("Starting initial backfill of all Stripe data...");
964
- const backfillResult = await this.stripeSync.syncBackfill({ object: "all" });
965
- const totalSynced = Object.values(backfillResult).reduce((sum, result) => sum + (result?.synced || 0), 0);
966
- console.log(`\u2713 Backfill complete: ${totalSynced} objects synced`);
967
- return {
968
- baseUrl,
969
- webhookUrl: webhook.url,
970
- webhookUuid: uuid
971
- };
972
986
  } catch (error) {
973
- if (error instanceof Error) {
974
- console.error("Failed to start Stripe Sync:", error.message);
975
- console.error(error.stack || "");
976
- } else {
977
- console.error("Failed to start Stripe Sync:", String(error));
978
- }
979
- await this.stop();
980
- throw error;
987
+ this.config.logger?.error(error, "Failed to upsert account to database");
988
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
989
+ throw new Error(`Failed to upsert account to database: ${errorMessage}`);
981
990
  }
982
991
  }
983
992
  /**
984
- * Stops all services and cleans up resources:
985
- * 1. Optionally deletes Stripe webhook endpoint from Stripe and database (based on keepWebhooksOnShutdown)
993
+ * Get the current account being synced. Uses database lookup by API key hash,
994
+ * with fallback to Stripe API if not found (first-time setup or new API key).
995
+ * @param objectAccountId - Optional account ID from event data (Connect scenarios)
986
996
  */
987
- async stop() {
988
- if (this.webhookId && this.stripeSync && !this.options.keepWebhooksOnShutdown) {
989
- try {
990
- await this.stripeSync.deleteManagedWebhook(this.webhookId);
991
- } catch (error) {
992
- console.error("Could not delete webhook:", error);
997
+ async getCurrentAccount(objectAccountId) {
998
+ const apiKeyHash = hashApiKey(this.config.stripeSecretKey);
999
+ try {
1000
+ const account = await this.postgresClient.getAccountByApiKeyHash(apiKeyHash);
1001
+ if (account) {
1002
+ return account;
993
1003
  }
1004
+ } catch (error) {
1005
+ this.config.logger?.warn(
1006
+ error,
1007
+ "Failed to lookup account by API key hash, falling back to API"
1008
+ );
1009
+ }
1010
+ try {
1011
+ const accountIdParam = objectAccountId || this.config.stripeAccountId;
1012
+ const account = accountIdParam ? await this.stripe.accounts.retrieve(accountIdParam) : await this.stripe.accounts.retrieve();
1013
+ await this.upsertAccount(account, apiKeyHash);
1014
+ return account;
1015
+ } catch (error) {
1016
+ this.config.logger?.error(error, "Failed to retrieve account from Stripe API");
1017
+ return null;
994
1018
  }
995
1019
  }
996
1020
  /**
997
- * Returns Express middleware for body parsing that automatically skips webhook routes.
998
- * This middleware applies JSON and URL-encoded parsers to all routes EXCEPT the webhook path,
999
- * which needs raw body for signature verification.
1000
- *
1001
- * @returns Express middleware function
1021
+ * Get all accounts that have been synced to the database
1002
1022
  */
1003
- getBodyParserMiddleware() {
1004
- const webhookPath = this.options.webhookPath;
1005
- return (req, res, next) => {
1006
- const path2 = req.path || req.url;
1007
- if (path2 && path2.startsWith(webhookPath)) {
1008
- return next();
1009
- }
1010
- import_express.default.json()(req, res, (err) => {
1011
- if (err) return next(err);
1012
- import_express.default.urlencoded({ extended: false })(req, res, next);
1013
- });
1014
- };
1023
+ async getAllSyncedAccounts() {
1024
+ try {
1025
+ const accountsData = await this.postgresClient.getAllAccounts();
1026
+ return accountsData;
1027
+ } catch (error) {
1028
+ this.config.logger?.error(error, "Failed to retrieve accounts from database");
1029
+ throw new Error("Failed to retrieve synced accounts from database");
1030
+ }
1015
1031
  }
1016
1032
  /**
1017
- * Mounts the Stripe webhook handler on the provided Express app.
1018
- * Applies raw body parser middleware for signature verification.
1019
- * IMPORTANT: Must be called BEFORE app.use(express.json()) to ensure raw body parsing.
1033
+ * DANGEROUS: Delete an account and all associated data from the database
1034
+ * This operation cannot be undone!
1035
+ *
1036
+ * @param accountId - The Stripe account ID to delete
1037
+ * @param options - Options for deletion behavior
1038
+ * @param options.dryRun - If true, only count records without deleting (default: false)
1039
+ * @param options.useTransaction - If true, use transaction for atomic deletion (default: true)
1040
+ * @returns Deletion summary with counts and warnings
1020
1041
  */
1021
- mountWebhook(app) {
1022
- const webhookRoute = `${this.options.webhookPath}/:uuid`;
1023
- app.use(webhookRoute, import_express.default.raw({ type: "application/json" }));
1024
- app.post(webhookRoute, async (req, res) => {
1025
- const sig = req.headers["stripe-signature"];
1026
- if (!sig || typeof sig !== "string") {
1027
- console.error("[Webhook] Missing stripe-signature header");
1028
- return res.status(400).send({ error: "Missing stripe-signature header" });
1042
+ async dangerouslyDeleteSyncedAccountData(accountId, options) {
1043
+ const dryRun = options?.dryRun ?? false;
1044
+ const useTransaction = options?.useTransaction ?? true;
1045
+ this.config.logger?.info(
1046
+ `${dryRun ? "Preview" : "Deleting"} account ${accountId} (transaction: ${useTransaction})`
1047
+ );
1048
+ try {
1049
+ const counts = await this.postgresClient.getAccountRecordCounts(accountId);
1050
+ const warnings = [];
1051
+ let totalRecords = 0;
1052
+ for (const [table, count] of Object.entries(counts)) {
1053
+ if (count > 0) {
1054
+ totalRecords += count;
1055
+ warnings.push(`Will delete ${count} ${table} record${count !== 1 ? "s" : ""}`);
1056
+ }
1029
1057
  }
1030
- const { uuid } = req.params;
1031
- const rawBody = req.body;
1032
- if (!rawBody || !Buffer.isBuffer(rawBody)) {
1033
- console.error("[Webhook] Body is not a Buffer!", {
1034
- hasBody: !!rawBody,
1035
- bodyType: typeof rawBody,
1036
- isBuffer: Buffer.isBuffer(rawBody),
1037
- bodyConstructor: rawBody?.constructor?.name
1038
- });
1039
- return res.status(400).send({ error: "Missing raw body for signature verification" });
1058
+ if (totalRecords > 1e5) {
1059
+ warnings.push(
1060
+ `Large dataset detected (${totalRecords} total records). Consider using useTransaction: false for better performance.`
1061
+ );
1040
1062
  }
1041
- try {
1042
- await this.stripeSync.processWebhook(rawBody, sig, uuid);
1043
- return res.status(200).send({ received: true });
1044
- } catch (error) {
1045
- console.error("[Webhook] Processing error:", error.message);
1046
- return res.status(400).send({ error: error.message });
1063
+ if (dryRun) {
1064
+ this.config.logger?.info(`Dry-run complete: ${totalRecords} total records would be deleted`);
1065
+ return {
1066
+ deletedAccountId: accountId,
1067
+ deletedRecordCounts: counts,
1068
+ warnings
1069
+ };
1047
1070
  }
1048
- });
1071
+ const deletionCounts = await this.postgresClient.deleteAccountWithCascade(
1072
+ accountId,
1073
+ useTransaction
1074
+ );
1075
+ this.config.logger?.info(
1076
+ `Successfully deleted account ${accountId} with ${totalRecords} total records`
1077
+ );
1078
+ return {
1079
+ deletedAccountId: accountId,
1080
+ deletedRecordCounts: deletionCounts,
1081
+ warnings
1082
+ };
1083
+ } catch (error) {
1084
+ this.config.logger?.error(error, `Failed to delete account ${accountId}`);
1085
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1086
+ throw new Error(`Failed to delete account ${accountId}: ${errorMessage}`);
1087
+ }
1049
1088
  }
1050
- };
1051
- var StripeSync = class {
1052
- constructor(config) {
1053
- this.config = config;
1054
- this.stripe = new import_stripe.default(config.stripeSecretKey, {
1055
- // https://github.com/stripe/stripe-node#configuration
1056
- // @ts-ignore
1057
- apiVersion: config.stripeApiVersion,
1058
- appInfo: {
1059
- name: "Stripe Postgres Sync"
1089
+ async processWebhook(payload, signature) {
1090
+ let webhookSecret = this.config.stripeWebhookSecret;
1091
+ if (!webhookSecret) {
1092
+ const accountId = await this.getAccountId();
1093
+ const result = await this.postgresClient.query(
1094
+ `SELECT secret FROM "stripe"."_managed_webhooks" WHERE account_id = $1 LIMIT 1`,
1095
+ [accountId]
1096
+ );
1097
+ if (result.rows.length > 0) {
1098
+ webhookSecret = result.rows[0].secret;
1060
1099
  }
1061
- });
1062
- this.config.logger?.info(
1063
- { autoExpandLists: config.autoExpandLists, stripeApiVersion: config.stripeApiVersion },
1064
- "StripeSync initialized"
1065
- );
1066
- const poolConfig = config.poolConfig ?? {};
1067
- if (config.databaseUrl) {
1068
- poolConfig.connectionString = config.databaseUrl;
1069
1100
  }
1070
- if (config.maxPostgresConnections) {
1071
- poolConfig.max = config.maxPostgresConnections;
1101
+ if (!webhookSecret) {
1102
+ throw new Error(
1103
+ "No webhook secret provided. Either create a managed webhook or configure stripeWebhookSecret."
1104
+ );
1072
1105
  }
1073
- if (poolConfig.max === void 0) {
1074
- poolConfig.max = 10;
1106
+ const event = await this.stripe.webhooks.constructEventAsync(payload, signature, webhookSecret);
1107
+ return this.processEvent(event);
1108
+ }
1109
+ // Event handler registry - maps event types to handler functions
1110
+ // Note: Uses 'any' for event parameter to allow handlers with specific Stripe event types
1111
+ // (e.g., CustomerDeletedEvent, ProductDeletedEvent) which TypeScript won't accept
1112
+ // as contravariant parameters when using the base Stripe.Event type
1113
+ eventHandlers = {
1114
+ "charge.captured": this.handleChargeEvent.bind(this),
1115
+ "charge.expired": this.handleChargeEvent.bind(this),
1116
+ "charge.failed": this.handleChargeEvent.bind(this),
1117
+ "charge.pending": this.handleChargeEvent.bind(this),
1118
+ "charge.refunded": this.handleChargeEvent.bind(this),
1119
+ "charge.succeeded": this.handleChargeEvent.bind(this),
1120
+ "charge.updated": this.handleChargeEvent.bind(this),
1121
+ "customer.deleted": this.handleCustomerDeletedEvent.bind(this),
1122
+ "customer.created": this.handleCustomerEvent.bind(this),
1123
+ "customer.updated": this.handleCustomerEvent.bind(this),
1124
+ "checkout.session.async_payment_failed": this.handleCheckoutSessionEvent.bind(this),
1125
+ "checkout.session.async_payment_succeeded": this.handleCheckoutSessionEvent.bind(this),
1126
+ "checkout.session.completed": this.handleCheckoutSessionEvent.bind(this),
1127
+ "checkout.session.expired": this.handleCheckoutSessionEvent.bind(this),
1128
+ "customer.subscription.created": this.handleSubscriptionEvent.bind(this),
1129
+ "customer.subscription.deleted": this.handleSubscriptionEvent.bind(this),
1130
+ "customer.subscription.paused": this.handleSubscriptionEvent.bind(this),
1131
+ "customer.subscription.pending_update_applied": this.handleSubscriptionEvent.bind(this),
1132
+ "customer.subscription.pending_update_expired": this.handleSubscriptionEvent.bind(this),
1133
+ "customer.subscription.trial_will_end": this.handleSubscriptionEvent.bind(this),
1134
+ "customer.subscription.resumed": this.handleSubscriptionEvent.bind(this),
1135
+ "customer.subscription.updated": this.handleSubscriptionEvent.bind(this),
1136
+ "customer.tax_id.updated": this.handleTaxIdEvent.bind(this),
1137
+ "customer.tax_id.created": this.handleTaxIdEvent.bind(this),
1138
+ "customer.tax_id.deleted": this.handleTaxIdDeletedEvent.bind(this),
1139
+ "invoice.created": this.handleInvoiceEvent.bind(this),
1140
+ "invoice.deleted": this.handleInvoiceEvent.bind(this),
1141
+ "invoice.finalized": this.handleInvoiceEvent.bind(this),
1142
+ "invoice.finalization_failed": this.handleInvoiceEvent.bind(this),
1143
+ "invoice.paid": this.handleInvoiceEvent.bind(this),
1144
+ "invoice.payment_action_required": this.handleInvoiceEvent.bind(this),
1145
+ "invoice.payment_failed": this.handleInvoiceEvent.bind(this),
1146
+ "invoice.payment_succeeded": this.handleInvoiceEvent.bind(this),
1147
+ "invoice.upcoming": this.handleInvoiceEvent.bind(this),
1148
+ "invoice.sent": this.handleInvoiceEvent.bind(this),
1149
+ "invoice.voided": this.handleInvoiceEvent.bind(this),
1150
+ "invoice.marked_uncollectible": this.handleInvoiceEvent.bind(this),
1151
+ "invoice.updated": this.handleInvoiceEvent.bind(this),
1152
+ "product.created": this.handleProductEvent.bind(this),
1153
+ "product.updated": this.handleProductEvent.bind(this),
1154
+ "product.deleted": this.handleProductDeletedEvent.bind(this),
1155
+ "price.created": this.handlePriceEvent.bind(this),
1156
+ "price.updated": this.handlePriceEvent.bind(this),
1157
+ "price.deleted": this.handlePriceDeletedEvent.bind(this),
1158
+ "plan.created": this.handlePlanEvent.bind(this),
1159
+ "plan.updated": this.handlePlanEvent.bind(this),
1160
+ "plan.deleted": this.handlePlanDeletedEvent.bind(this),
1161
+ "setup_intent.canceled": this.handleSetupIntentEvent.bind(this),
1162
+ "setup_intent.created": this.handleSetupIntentEvent.bind(this),
1163
+ "setup_intent.requires_action": this.handleSetupIntentEvent.bind(this),
1164
+ "setup_intent.setup_failed": this.handleSetupIntentEvent.bind(this),
1165
+ "setup_intent.succeeded": this.handleSetupIntentEvent.bind(this),
1166
+ "subscription_schedule.aborted": this.handleSubscriptionScheduleEvent.bind(this),
1167
+ "subscription_schedule.canceled": this.handleSubscriptionScheduleEvent.bind(this),
1168
+ "subscription_schedule.completed": this.handleSubscriptionScheduleEvent.bind(this),
1169
+ "subscription_schedule.created": this.handleSubscriptionScheduleEvent.bind(this),
1170
+ "subscription_schedule.expiring": this.handleSubscriptionScheduleEvent.bind(this),
1171
+ "subscription_schedule.released": this.handleSubscriptionScheduleEvent.bind(this),
1172
+ "subscription_schedule.updated": this.handleSubscriptionScheduleEvent.bind(this),
1173
+ "payment_method.attached": this.handlePaymentMethodEvent.bind(this),
1174
+ "payment_method.automatically_updated": this.handlePaymentMethodEvent.bind(this),
1175
+ "payment_method.detached": this.handlePaymentMethodEvent.bind(this),
1176
+ "payment_method.updated": this.handlePaymentMethodEvent.bind(this),
1177
+ "charge.dispute.created": this.handleDisputeEvent.bind(this),
1178
+ "charge.dispute.funds_reinstated": this.handleDisputeEvent.bind(this),
1179
+ "charge.dispute.funds_withdrawn": this.handleDisputeEvent.bind(this),
1180
+ "charge.dispute.updated": this.handleDisputeEvent.bind(this),
1181
+ "charge.dispute.closed": this.handleDisputeEvent.bind(this),
1182
+ "payment_intent.amount_capturable_updated": this.handlePaymentIntentEvent.bind(this),
1183
+ "payment_intent.canceled": this.handlePaymentIntentEvent.bind(this),
1184
+ "payment_intent.created": this.handlePaymentIntentEvent.bind(this),
1185
+ "payment_intent.partially_funded": this.handlePaymentIntentEvent.bind(this),
1186
+ "payment_intent.payment_failed": this.handlePaymentIntentEvent.bind(this),
1187
+ "payment_intent.processing": this.handlePaymentIntentEvent.bind(this),
1188
+ "payment_intent.requires_action": this.handlePaymentIntentEvent.bind(this),
1189
+ "payment_intent.succeeded": this.handlePaymentIntentEvent.bind(this),
1190
+ "credit_note.created": this.handleCreditNoteEvent.bind(this),
1191
+ "credit_note.updated": this.handleCreditNoteEvent.bind(this),
1192
+ "credit_note.voided": this.handleCreditNoteEvent.bind(this),
1193
+ "radar.early_fraud_warning.created": this.handleEarlyFraudWarningEvent.bind(this),
1194
+ "radar.early_fraud_warning.updated": this.handleEarlyFraudWarningEvent.bind(this),
1195
+ "refund.created": this.handleRefundEvent.bind(this),
1196
+ "refund.failed": this.handleRefundEvent.bind(this),
1197
+ "refund.updated": this.handleRefundEvent.bind(this),
1198
+ "charge.refund.updated": this.handleRefundEvent.bind(this),
1199
+ "review.closed": this.handleReviewEvent.bind(this),
1200
+ "review.opened": this.handleReviewEvent.bind(this),
1201
+ "entitlements.active_entitlement_summary.updated": this.handleEntitlementSummaryEvent.bind(this)
1202
+ };
1203
+ // Resource registry - maps SyncObject → list/upsert operations for processNext()
1204
+ // Complements eventHandlers which maps event types → handlers for webhooks
1205
+ // Both registries share the same underlying upsert methods
1206
+ // Order field determines backfill sequence - parents before children for FK dependencies
1207
+ resourceRegistry = {
1208
+ product: {
1209
+ order: 1,
1210
+ // No dependencies
1211
+ listFn: (p) => this.stripe.products.list(p),
1212
+ upsertFn: (items, id) => this.upsertProducts(items, id),
1213
+ supportsCreatedFilter: true
1214
+ },
1215
+ price: {
1216
+ order: 2,
1217
+ // Depends on product
1218
+ listFn: (p) => this.stripe.prices.list(p),
1219
+ upsertFn: (items, id, bf) => this.upsertPrices(items, id, bf),
1220
+ supportsCreatedFilter: true
1221
+ },
1222
+ plan: {
1223
+ order: 3,
1224
+ // Depends on product
1225
+ listFn: (p) => this.stripe.plans.list(p),
1226
+ upsertFn: (items, id, bf) => this.upsertPlans(items, id, bf),
1227
+ supportsCreatedFilter: true
1228
+ },
1229
+ customer: {
1230
+ order: 4,
1231
+ // No dependencies
1232
+ listFn: (p) => this.stripe.customers.list(p),
1233
+ upsertFn: (items, id) => this.upsertCustomers(items, id),
1234
+ supportsCreatedFilter: true
1235
+ },
1236
+ subscription: {
1237
+ order: 5,
1238
+ // Depends on customer, price
1239
+ listFn: (p) => this.stripe.subscriptions.list(p),
1240
+ upsertFn: (items, id, bf) => this.upsertSubscriptions(items, id, bf),
1241
+ supportsCreatedFilter: true
1242
+ },
1243
+ subscription_schedules: {
1244
+ order: 6,
1245
+ // Depends on customer
1246
+ listFn: (p) => this.stripe.subscriptionSchedules.list(p),
1247
+ upsertFn: (items, id, bf) => this.upsertSubscriptionSchedules(items, id, bf),
1248
+ supportsCreatedFilter: true
1249
+ },
1250
+ invoice: {
1251
+ order: 7,
1252
+ // Depends on customer, subscription
1253
+ listFn: (p) => this.stripe.invoices.list(p),
1254
+ upsertFn: (items, id, bf) => this.upsertInvoices(items, id, bf),
1255
+ supportsCreatedFilter: true
1256
+ },
1257
+ charge: {
1258
+ order: 8,
1259
+ // Depends on customer, invoice
1260
+ listFn: (p) => this.stripe.charges.list(p),
1261
+ upsertFn: (items, id, bf) => this.upsertCharges(items, id, bf),
1262
+ supportsCreatedFilter: true
1263
+ },
1264
+ setup_intent: {
1265
+ order: 9,
1266
+ // Depends on customer
1267
+ listFn: (p) => this.stripe.setupIntents.list(p),
1268
+ upsertFn: (items, id, bf) => this.upsertSetupIntents(items, id, bf),
1269
+ supportsCreatedFilter: true
1270
+ },
1271
+ payment_method: {
1272
+ order: 10,
1273
+ // Depends on customer (special: iterates customers)
1274
+ listFn: (p) => this.stripe.paymentMethods.list(p),
1275
+ upsertFn: (items, id, bf) => this.upsertPaymentMethods(items, id, bf),
1276
+ supportsCreatedFilter: false
1277
+ // Requires customer param, can't filter by created
1278
+ },
1279
+ payment_intent: {
1280
+ order: 11,
1281
+ // Depends on customer
1282
+ listFn: (p) => this.stripe.paymentIntents.list(p),
1283
+ upsertFn: (items, id, bf) => this.upsertPaymentIntents(items, id, bf),
1284
+ supportsCreatedFilter: true
1285
+ },
1286
+ tax_id: {
1287
+ order: 12,
1288
+ // Depends on customer
1289
+ listFn: (p) => this.stripe.taxIds.list(p),
1290
+ upsertFn: (items, id, bf) => this.upsertTaxIds(items, id, bf),
1291
+ supportsCreatedFilter: false
1292
+ // taxIds don't support created filter
1293
+ },
1294
+ credit_note: {
1295
+ order: 13,
1296
+ // Depends on invoice
1297
+ listFn: (p) => this.stripe.creditNotes.list(p),
1298
+ upsertFn: (items, id, bf) => this.upsertCreditNotes(items, id, bf),
1299
+ supportsCreatedFilter: false
1300
+ // credit_notes don't support created filter
1301
+ },
1302
+ dispute: {
1303
+ order: 14,
1304
+ // Depends on charge
1305
+ listFn: (p) => this.stripe.disputes.list(p),
1306
+ upsertFn: (items, id, bf) => this.upsertDisputes(items, id, bf),
1307
+ supportsCreatedFilter: true
1308
+ },
1309
+ early_fraud_warning: {
1310
+ order: 15,
1311
+ // Depends on charge
1312
+ listFn: (p) => this.stripe.radar.earlyFraudWarnings.list(p),
1313
+ upsertFn: (items, id) => this.upsertEarlyFraudWarning(items, id),
1314
+ supportsCreatedFilter: true
1315
+ },
1316
+ refund: {
1317
+ order: 16,
1318
+ // Depends on charge
1319
+ listFn: (p) => this.stripe.refunds.list(p),
1320
+ upsertFn: (items, id, bf) => this.upsertRefunds(items, id, bf),
1321
+ supportsCreatedFilter: true
1322
+ },
1323
+ checkout_sessions: {
1324
+ order: 17,
1325
+ // Depends on customer (optional)
1326
+ listFn: (p) => this.stripe.checkout.sessions.list(p),
1327
+ upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
1328
+ supportsCreatedFilter: true
1075
1329
  }
1076
- if (poolConfig.keepAlive === void 0) {
1077
- poolConfig.keepAlive = true;
1330
+ };
1331
+ async processEvent(event) {
1332
+ const objectAccountId = event.data?.object && typeof event.data.object === "object" && "account" in event.data.object ? event.data.object.account : void 0;
1333
+ const accountId = await this.getAccountId(objectAccountId);
1334
+ await this.getCurrentAccount();
1335
+ const handler = this.eventHandlers[event.type];
1336
+ if (handler) {
1337
+ const entityId = event.data?.object && typeof event.data.object === "object" && "id" in event.data.object ? event.data.object.id : "unknown";
1338
+ this.config.logger?.info(`Received webhook ${event.id}: ${event.type} for ${entityId}`);
1339
+ await handler(event, accountId);
1340
+ } else {
1341
+ this.config.logger?.warn(
1342
+ `Received unhandled webhook event: ${event.type} (${event.id}). Ignoring.`
1343
+ );
1078
1344
  }
1079
- this.postgresClient = new PostgresClient({
1080
- schema: config.schema || DEFAULT_SCHEMA,
1081
- poolConfig
1082
- });
1083
1345
  }
1084
- stripe;
1085
- postgresClient;
1086
- async processWebhook(payload, signature, uuid) {
1087
- const result = await this.postgresClient.query(
1088
- `SELECT secret FROM "${this.config.schema || DEFAULT_SCHEMA}"."_managed_webhooks" WHERE uuid = $1`,
1089
- [uuid]
1346
+ /**
1347
+ * Returns an array of all webhook event types that this sync engine can handle.
1348
+ * Useful for configuring webhook endpoints with specific event subscriptions.
1349
+ */
1350
+ getSupportedEventTypes() {
1351
+ return Object.keys(
1352
+ this.eventHandlers
1353
+ ).sort();
1354
+ }
1355
+ /**
1356
+ * Returns an array of all object types that can be synced via processNext/processUntilDone.
1357
+ * Ordered for backfill: parents before children (products before prices, customers before subscriptions).
1358
+ * Order is determined by the `order` field in resourceRegistry.
1359
+ */
1360
+ getSupportedSyncObjects() {
1361
+ return Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1362
+ }
1363
+ // Event handler methods
1364
+ async handleChargeEvent(event, accountId) {
1365
+ const { entity: charge, refetched } = await this.fetchOrUseWebhookData(
1366
+ event.data.object,
1367
+ (id) => this.stripe.charges.retrieve(id),
1368
+ (charge2) => charge2.status === "failed" || charge2.status === "succeeded"
1090
1369
  );
1091
- if (result.rows.length === 0) {
1092
- throw new Error(`No managed webhook found with UUID: ${uuid}`);
1093
- }
1094
- const webhookSecret = result.rows[0].secret;
1095
- const event = await this.stripe.webhooks.constructEventAsync(
1096
- payload,
1097
- signature,
1098
- webhookSecret
1370
+ await this.upsertCharges([charge], accountId, false, this.getSyncTimestamp(event, refetched));
1371
+ }
1372
+ async handleCustomerDeletedEvent(event, accountId) {
1373
+ const customer = {
1374
+ id: event.data.object.id,
1375
+ object: "customer",
1376
+ deleted: true
1377
+ };
1378
+ await this.upsertCustomers([customer], accountId, this.getSyncTimestamp(event, false));
1379
+ }
1380
+ async handleCustomerEvent(event, accountId) {
1381
+ const { entity: customer, refetched } = await this.fetchOrUseWebhookData(
1382
+ event.data.object,
1383
+ (id) => this.stripe.customers.retrieve(id),
1384
+ (customer2) => customer2.deleted === true
1099
1385
  );
1100
- return this.processEvent(event);
1386
+ await this.upsertCustomers([customer], accountId, this.getSyncTimestamp(event, refetched));
1101
1387
  }
1102
- async processEvent(event) {
1103
- switch (event.type) {
1104
- case "charge.captured":
1105
- case "charge.expired":
1106
- case "charge.failed":
1107
- case "charge.pending":
1108
- case "charge.refunded":
1109
- case "charge.succeeded":
1110
- case "charge.updated": {
1111
- const { entity: charge, refetched } = await this.fetchOrUseWebhookData(
1112
- event.data.object,
1113
- (id) => this.stripe.charges.retrieve(id),
1114
- (charge2) => charge2.status === "failed" || charge2.status === "succeeded"
1115
- );
1116
- this.config.logger?.info(
1117
- `Received webhook ${event.id}: ${event.type} for charge ${charge.id}`
1118
- );
1119
- await this.upsertCharges([charge], false, this.getSyncTimestamp(event, refetched));
1120
- break;
1121
- }
1122
- case "customer.deleted": {
1123
- const customer = {
1124
- id: event.data.object.id,
1125
- object: "customer",
1126
- deleted: true
1127
- };
1128
- this.config.logger?.info(
1129
- `Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
1130
- );
1131
- await this.upsertCustomers([customer], this.getSyncTimestamp(event, false));
1132
- break;
1133
- }
1134
- case "checkout.session.async_payment_failed":
1135
- case "checkout.session.async_payment_succeeded":
1136
- case "checkout.session.completed":
1137
- case "checkout.session.expired": {
1138
- const { entity: checkoutSession, refetched } = await this.fetchOrUseWebhookData(
1139
- event.data.object,
1140
- (id) => this.stripe.checkout.sessions.retrieve(id)
1141
- );
1142
- this.config.logger?.info(
1143
- `Received webhook ${event.id}: ${event.type} for checkout session ${checkoutSession.id}`
1144
- );
1145
- await this.upsertCheckoutSessions(
1146
- [checkoutSession],
1147
- false,
1148
- this.getSyncTimestamp(event, refetched)
1149
- );
1150
- break;
1151
- }
1152
- case "customer.created":
1153
- case "customer.updated": {
1154
- const { entity: customer, refetched } = await this.fetchOrUseWebhookData(
1155
- event.data.object,
1156
- (id) => this.stripe.customers.retrieve(id),
1157
- (customer2) => customer2.deleted === true
1158
- );
1159
- this.config.logger?.info(
1160
- `Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
1161
- );
1162
- await this.upsertCustomers([customer], this.getSyncTimestamp(event, refetched));
1163
- break;
1164
- }
1165
- case "customer.subscription.created":
1166
- case "customer.subscription.deleted":
1167
- // Soft delete using `status = canceled`
1168
- case "customer.subscription.paused":
1169
- case "customer.subscription.pending_update_applied":
1170
- case "customer.subscription.pending_update_expired":
1171
- case "customer.subscription.trial_will_end":
1172
- case "customer.subscription.resumed":
1173
- case "customer.subscription.updated": {
1174
- const { entity: subscription, refetched } = await this.fetchOrUseWebhookData(
1175
- event.data.object,
1176
- (id) => this.stripe.subscriptions.retrieve(id),
1177
- (subscription2) => subscription2.status === "canceled" || subscription2.status === "incomplete_expired"
1178
- );
1179
- this.config.logger?.info(
1180
- `Received webhook ${event.id}: ${event.type} for subscription ${subscription.id}`
1181
- );
1182
- await this.upsertSubscriptions(
1183
- [subscription],
1184
- false,
1185
- this.getSyncTimestamp(event, refetched)
1186
- );
1187
- break;
1188
- }
1189
- case "customer.tax_id.updated":
1190
- case "customer.tax_id.created": {
1191
- const { entity: taxId, refetched } = await this.fetchOrUseWebhookData(
1192
- event.data.object,
1193
- (id) => this.stripe.taxIds.retrieve(id)
1194
- );
1195
- this.config.logger?.info(
1196
- `Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
1197
- );
1198
- await this.upsertTaxIds([taxId], false, this.getSyncTimestamp(event, refetched));
1199
- break;
1200
- }
1201
- case "customer.tax_id.deleted": {
1202
- const taxId = event.data.object;
1203
- this.config.logger?.info(
1204
- `Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
1205
- );
1206
- await this.deleteTaxId(taxId.id);
1207
- break;
1208
- }
1209
- case "invoice.created":
1210
- case "invoice.deleted":
1211
- case "invoice.finalized":
1212
- case "invoice.finalization_failed":
1213
- case "invoice.paid":
1214
- case "invoice.payment_action_required":
1215
- case "invoice.payment_failed":
1216
- case "invoice.payment_succeeded":
1217
- case "invoice.upcoming":
1218
- case "invoice.sent":
1219
- case "invoice.voided":
1220
- case "invoice.marked_uncollectible":
1221
- case "invoice.updated": {
1222
- const { entity: invoice, refetched } = await this.fetchOrUseWebhookData(
1223
- event.data.object,
1224
- (id) => this.stripe.invoices.retrieve(id),
1225
- (invoice2) => invoice2.status === "void"
1226
- );
1227
- this.config.logger?.info(
1228
- `Received webhook ${event.id}: ${event.type} for invoice ${invoice.id}`
1229
- );
1230
- await this.upsertInvoices([invoice], false, this.getSyncTimestamp(event, refetched));
1231
- break;
1232
- }
1233
- case "product.created":
1234
- case "product.updated": {
1235
- try {
1236
- const { entity: product, refetched } = await this.fetchOrUseWebhookData(
1237
- event.data.object,
1238
- (id) => this.stripe.products.retrieve(id)
1239
- );
1240
- this.config.logger?.info(
1241
- `Received webhook ${event.id}: ${event.type} for product ${product.id}`
1242
- );
1243
- await this.upsertProducts([product], this.getSyncTimestamp(event, refetched));
1244
- } catch (err) {
1245
- if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
1246
- await this.deleteProduct(event.data.object.id);
1247
- } else {
1248
- throw err;
1249
- }
1250
- }
1251
- break;
1252
- }
1253
- case "product.deleted": {
1388
+ async handleCheckoutSessionEvent(event, accountId) {
1389
+ const { entity: checkoutSession, refetched } = await this.fetchOrUseWebhookData(
1390
+ event.data.object,
1391
+ (id) => this.stripe.checkout.sessions.retrieve(id)
1392
+ );
1393
+ await this.upsertCheckoutSessions(
1394
+ [checkoutSession],
1395
+ accountId,
1396
+ false,
1397
+ this.getSyncTimestamp(event, refetched)
1398
+ );
1399
+ }
1400
+ async handleSubscriptionEvent(event, accountId) {
1401
+ const { entity: subscription, refetched } = await this.fetchOrUseWebhookData(
1402
+ event.data.object,
1403
+ (id) => this.stripe.subscriptions.retrieve(id),
1404
+ (subscription2) => subscription2.status === "canceled" || subscription2.status === "incomplete_expired"
1405
+ );
1406
+ await this.upsertSubscriptions(
1407
+ [subscription],
1408
+ accountId,
1409
+ false,
1410
+ this.getSyncTimestamp(event, refetched)
1411
+ );
1412
+ }
1413
+ async handleTaxIdEvent(event, accountId) {
1414
+ const { entity: taxId, refetched } = await this.fetchOrUseWebhookData(
1415
+ event.data.object,
1416
+ (id) => this.stripe.taxIds.retrieve(id)
1417
+ );
1418
+ await this.upsertTaxIds([taxId], accountId, false, this.getSyncTimestamp(event, refetched));
1419
+ }
1420
+ async handleTaxIdDeletedEvent(event, _accountId) {
1421
+ const taxId = event.data.object;
1422
+ await this.deleteTaxId(taxId.id);
1423
+ }
1424
+ async handleInvoiceEvent(event, accountId) {
1425
+ const { entity: invoice, refetched } = await this.fetchOrUseWebhookData(
1426
+ event.data.object,
1427
+ (id) => this.stripe.invoices.retrieve(id),
1428
+ (invoice2) => invoice2.status === "void"
1429
+ );
1430
+ await this.upsertInvoices([invoice], accountId, false, this.getSyncTimestamp(event, refetched));
1431
+ }
1432
+ async handleProductEvent(event, accountId) {
1433
+ try {
1434
+ const { entity: product, refetched } = await this.fetchOrUseWebhookData(
1435
+ event.data.object,
1436
+ (id) => this.stripe.products.retrieve(id)
1437
+ );
1438
+ await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
1439
+ } catch (err) {
1440
+ if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1254
1441
  const product = event.data.object;
1255
- this.config.logger?.info(
1256
- `Received webhook ${event.id}: ${event.type} for product ${product.id}`
1257
- );
1258
1442
  await this.deleteProduct(product.id);
1259
- break;
1260
- }
1261
- case "price.created":
1262
- case "price.updated": {
1263
- try {
1264
- const { entity: price, refetched } = await this.fetchOrUseWebhookData(
1265
- event.data.object,
1266
- (id) => this.stripe.prices.retrieve(id)
1267
- );
1268
- this.config.logger?.info(
1269
- `Received webhook ${event.id}: ${event.type} for price ${price.id}`
1270
- );
1271
- await this.upsertPrices([price], false, this.getSyncTimestamp(event, refetched));
1272
- } catch (err) {
1273
- if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
1274
- await this.deletePrice(event.data.object.id);
1275
- } else {
1276
- throw err;
1277
- }
1278
- }
1279
- break;
1443
+ } else {
1444
+ throw err;
1280
1445
  }
1281
- case "price.deleted": {
1446
+ }
1447
+ }
1448
+ async handleProductDeletedEvent(event, _accountId) {
1449
+ const product = event.data.object;
1450
+ await this.deleteProduct(product.id);
1451
+ }
1452
+ async handlePriceEvent(event, accountId) {
1453
+ try {
1454
+ const { entity: price, refetched } = await this.fetchOrUseWebhookData(
1455
+ event.data.object,
1456
+ (id) => this.stripe.prices.retrieve(id)
1457
+ );
1458
+ await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
1459
+ } catch (err) {
1460
+ if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1282
1461
  const price = event.data.object;
1283
- this.config.logger?.info(
1284
- `Received webhook ${event.id}: ${event.type} for price ${price.id}`
1285
- );
1286
1462
  await this.deletePrice(price.id);
1287
- break;
1288
- }
1289
- case "plan.created":
1290
- case "plan.updated": {
1291
- try {
1292
- const { entity: plan, refetched } = await this.fetchOrUseWebhookData(
1293
- event.data.object,
1294
- (id) => this.stripe.plans.retrieve(id)
1295
- );
1296
- this.config.logger?.info(
1297
- `Received webhook ${event.id}: ${event.type} for plan ${plan.id}`
1298
- );
1299
- await this.upsertPlans([plan], false, this.getSyncTimestamp(event, refetched));
1300
- } catch (err) {
1301
- if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
1302
- await this.deletePlan(event.data.object.id);
1303
- } else {
1304
- throw err;
1305
- }
1306
- }
1307
- break;
1463
+ } else {
1464
+ throw err;
1308
1465
  }
1309
- case "plan.deleted": {
1466
+ }
1467
+ }
1468
+ async handlePriceDeletedEvent(event, _accountId) {
1469
+ const price = event.data.object;
1470
+ await this.deletePrice(price.id);
1471
+ }
1472
+ async handlePlanEvent(event, accountId) {
1473
+ try {
1474
+ const { entity: plan, refetched } = await this.fetchOrUseWebhookData(
1475
+ event.data.object,
1476
+ (id) => this.stripe.plans.retrieve(id)
1477
+ );
1478
+ await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
1479
+ } catch (err) {
1480
+ if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1310
1481
  const plan = event.data.object;
1311
- this.config.logger?.info(`Received webhook ${event.id}: ${event.type} for plan ${plan.id}`);
1312
1482
  await this.deletePlan(plan.id);
1313
- break;
1314
- }
1315
- case "setup_intent.canceled":
1316
- case "setup_intent.created":
1317
- case "setup_intent.requires_action":
1318
- case "setup_intent.setup_failed":
1319
- case "setup_intent.succeeded": {
1320
- const { entity: setupIntent, refetched } = await this.fetchOrUseWebhookData(
1321
- event.data.object,
1322
- (id) => this.stripe.setupIntents.retrieve(id),
1323
- (setupIntent2) => setupIntent2.status === "canceled" || setupIntent2.status === "succeeded"
1324
- );
1325
- this.config.logger?.info(
1326
- `Received webhook ${event.id}: ${event.type} for setupIntent ${setupIntent.id}`
1327
- );
1328
- await this.upsertSetupIntents([setupIntent], false, this.getSyncTimestamp(event, refetched));
1329
- break;
1330
- }
1331
- case "subscription_schedule.aborted":
1332
- case "subscription_schedule.canceled":
1333
- case "subscription_schedule.completed":
1334
- case "subscription_schedule.created":
1335
- case "subscription_schedule.expiring":
1336
- case "subscription_schedule.released":
1337
- case "subscription_schedule.updated": {
1338
- const { entity: subscriptionSchedule, refetched } = await this.fetchOrUseWebhookData(
1339
- event.data.object,
1340
- (id) => this.stripe.subscriptionSchedules.retrieve(id),
1341
- (schedule) => schedule.status === "canceled" || schedule.status === "completed"
1342
- );
1343
- this.config.logger?.info(
1344
- `Received webhook ${event.id}: ${event.type} for subscriptionSchedule ${subscriptionSchedule.id}`
1345
- );
1346
- await this.upsertSubscriptionSchedules(
1347
- [subscriptionSchedule],
1348
- false,
1349
- this.getSyncTimestamp(event, refetched)
1350
- );
1351
- break;
1352
- }
1353
- case "payment_method.attached":
1354
- case "payment_method.automatically_updated":
1355
- case "payment_method.detached":
1356
- case "payment_method.updated": {
1357
- const { entity: paymentMethod, refetched } = await this.fetchOrUseWebhookData(
1358
- event.data.object,
1359
- (id) => this.stripe.paymentMethods.retrieve(id)
1360
- );
1361
- this.config.logger?.info(
1362
- `Received webhook ${event.id}: ${event.type} for paymentMethod ${paymentMethod.id}`
1363
- );
1364
- await this.upsertPaymentMethods(
1365
- [paymentMethod],
1366
- false,
1367
- this.getSyncTimestamp(event, refetched)
1368
- );
1369
- break;
1370
- }
1371
- case "charge.dispute.created":
1372
- case "charge.dispute.funds_reinstated":
1373
- case "charge.dispute.funds_withdrawn":
1374
- case "charge.dispute.updated":
1375
- case "charge.dispute.closed": {
1376
- const { entity: dispute, refetched } = await this.fetchOrUseWebhookData(
1377
- event.data.object,
1378
- (id) => this.stripe.disputes.retrieve(id),
1379
- (dispute2) => dispute2.status === "won" || dispute2.status === "lost"
1380
- );
1381
- this.config.logger?.info(
1382
- `Received webhook ${event.id}: ${event.type} for dispute ${dispute.id}`
1383
- );
1384
- await this.upsertDisputes([dispute], false, this.getSyncTimestamp(event, refetched));
1385
- break;
1386
- }
1387
- case "payment_intent.amount_capturable_updated":
1388
- case "payment_intent.canceled":
1389
- case "payment_intent.created":
1390
- case "payment_intent.partially_funded":
1391
- case "payment_intent.payment_failed":
1392
- case "payment_intent.processing":
1393
- case "payment_intent.requires_action":
1394
- case "payment_intent.succeeded": {
1395
- const { entity: paymentIntent, refetched } = await this.fetchOrUseWebhookData(
1396
- event.data.object,
1397
- (id) => this.stripe.paymentIntents.retrieve(id),
1398
- // Final states - do not re-fetch from API
1399
- (entity) => entity.status === "canceled" || entity.status === "succeeded"
1400
- );
1401
- this.config.logger?.info(
1402
- `Received webhook ${event.id}: ${event.type} for paymentIntent ${paymentIntent.id}`
1403
- );
1404
- await this.upsertPaymentIntents(
1405
- [paymentIntent],
1406
- false,
1407
- this.getSyncTimestamp(event, refetched)
1408
- );
1409
- break;
1410
- }
1411
- case "credit_note.created":
1412
- case "credit_note.updated":
1413
- case "credit_note.voided": {
1414
- const { entity: creditNote, refetched } = await this.fetchOrUseWebhookData(
1415
- event.data.object,
1416
- (id) => this.stripe.creditNotes.retrieve(id),
1417
- (creditNote2) => creditNote2.status === "void"
1418
- );
1419
- this.config.logger?.info(
1420
- `Received webhook ${event.id}: ${event.type} for creditNote ${creditNote.id}`
1421
- );
1422
- await this.upsertCreditNotes([creditNote], false, this.getSyncTimestamp(event, refetched));
1423
- break;
1424
- }
1425
- case "radar.early_fraud_warning.created":
1426
- case "radar.early_fraud_warning.updated": {
1427
- const { entity: earlyFraudWarning, refetched } = await this.fetchOrUseWebhookData(
1428
- event.data.object,
1429
- (id) => this.stripe.radar.earlyFraudWarnings.retrieve(id)
1430
- );
1431
- this.config.logger?.info(
1432
- `Received webhook ${event.id}: ${event.type} for earlyFraudWarning ${earlyFraudWarning.id}`
1433
- );
1434
- await this.upsertEarlyFraudWarning(
1435
- [earlyFraudWarning],
1436
- false,
1437
- this.getSyncTimestamp(event, refetched)
1438
- );
1439
- break;
1440
- }
1441
- case "refund.created":
1442
- case "refund.failed":
1443
- case "refund.updated":
1444
- case "charge.refund.updated": {
1445
- const { entity: refund, refetched } = await this.fetchOrUseWebhookData(
1446
- event.data.object,
1447
- (id) => this.stripe.refunds.retrieve(id)
1448
- );
1449
- this.config.logger?.info(
1450
- `Received webhook ${event.id}: ${event.type} for refund ${refund.id}`
1451
- );
1452
- await this.upsertRefunds([refund], false, this.getSyncTimestamp(event, refetched));
1453
- break;
1454
- }
1455
- case "review.closed":
1456
- case "review.opened": {
1457
- const { entity: review, refetched } = await this.fetchOrUseWebhookData(
1458
- event.data.object,
1459
- (id) => this.stripe.reviews.retrieve(id)
1460
- );
1461
- this.config.logger?.info(
1462
- `Received webhook ${event.id}: ${event.type} for review ${review.id}`
1463
- );
1464
- await this.upsertReviews([review], false, this.getSyncTimestamp(event, refetched));
1465
- break;
1466
- }
1467
- case "entitlements.active_entitlement_summary.updated": {
1468
- const activeEntitlementSummary = event.data.object;
1469
- let entitlements = activeEntitlementSummary.entitlements;
1470
- let refetched = false;
1471
- if (this.config.revalidateObjectsViaStripeApi?.includes("entitlements")) {
1472
- const { lastResponse, ...rest } = await this.stripe.entitlements.activeEntitlements.list({
1473
- customer: activeEntitlementSummary.customer
1474
- });
1475
- entitlements = rest;
1476
- refetched = true;
1477
- }
1478
- this.config.logger?.info(
1479
- `Received webhook ${event.id}: ${event.type} for activeEntitlementSummary for customer ${activeEntitlementSummary.customer}`
1480
- );
1481
- await this.deleteRemovedActiveEntitlements(
1482
- activeEntitlementSummary.customer,
1483
- entitlements.data.map((entitlement) => entitlement.id)
1484
- );
1485
- await this.upsertActiveEntitlements(
1486
- activeEntitlementSummary.customer,
1487
- entitlements.data,
1488
- false,
1489
- this.getSyncTimestamp(event, refetched)
1490
- );
1491
- break;
1483
+ } else {
1484
+ throw err;
1492
1485
  }
1493
- default:
1494
- throw new Error("Unhandled webhook event");
1495
1486
  }
1496
1487
  }
1488
+ async handlePlanDeletedEvent(event, _accountId) {
1489
+ const plan = event.data.object;
1490
+ await this.deletePlan(plan.id);
1491
+ }
1492
+ async handleSetupIntentEvent(event, accountId) {
1493
+ const { entity: setupIntent, refetched } = await this.fetchOrUseWebhookData(
1494
+ event.data.object,
1495
+ (id) => this.stripe.setupIntents.retrieve(id),
1496
+ (setupIntent2) => setupIntent2.status === "canceled" || setupIntent2.status === "succeeded"
1497
+ );
1498
+ await this.upsertSetupIntents(
1499
+ [setupIntent],
1500
+ accountId,
1501
+ false,
1502
+ this.getSyncTimestamp(event, refetched)
1503
+ );
1504
+ }
1505
+ async handleSubscriptionScheduleEvent(event, accountId) {
1506
+ const { entity: subscriptionSchedule, refetched } = await this.fetchOrUseWebhookData(
1507
+ event.data.object,
1508
+ (id) => this.stripe.subscriptionSchedules.retrieve(id),
1509
+ (schedule) => schedule.status === "canceled" || schedule.status === "completed"
1510
+ );
1511
+ await this.upsertSubscriptionSchedules(
1512
+ [subscriptionSchedule],
1513
+ accountId,
1514
+ false,
1515
+ this.getSyncTimestamp(event, refetched)
1516
+ );
1517
+ }
1518
+ async handlePaymentMethodEvent(event, accountId) {
1519
+ const { entity: paymentMethod, refetched } = await this.fetchOrUseWebhookData(
1520
+ event.data.object,
1521
+ (id) => this.stripe.paymentMethods.retrieve(id)
1522
+ );
1523
+ await this.upsertPaymentMethods(
1524
+ [paymentMethod],
1525
+ accountId,
1526
+ false,
1527
+ this.getSyncTimestamp(event, refetched)
1528
+ );
1529
+ }
1530
+ async handleDisputeEvent(event, accountId) {
1531
+ const { entity: dispute, refetched } = await this.fetchOrUseWebhookData(
1532
+ event.data.object,
1533
+ (id) => this.stripe.disputes.retrieve(id),
1534
+ (dispute2) => dispute2.status === "won" || dispute2.status === "lost"
1535
+ );
1536
+ await this.upsertDisputes([dispute], accountId, false, this.getSyncTimestamp(event, refetched));
1537
+ }
1538
+ async handlePaymentIntentEvent(event, accountId) {
1539
+ const { entity: paymentIntent, refetched } = await this.fetchOrUseWebhookData(
1540
+ event.data.object,
1541
+ (id) => this.stripe.paymentIntents.retrieve(id),
1542
+ // Final states - do not re-fetch from API
1543
+ (entity) => entity.status === "canceled" || entity.status === "succeeded"
1544
+ );
1545
+ await this.upsertPaymentIntents(
1546
+ [paymentIntent],
1547
+ accountId,
1548
+ false,
1549
+ this.getSyncTimestamp(event, refetched)
1550
+ );
1551
+ }
1552
+ async handleCreditNoteEvent(event, accountId) {
1553
+ const { entity: creditNote, refetched } = await this.fetchOrUseWebhookData(
1554
+ event.data.object,
1555
+ (id) => this.stripe.creditNotes.retrieve(id),
1556
+ (creditNote2) => creditNote2.status === "void"
1557
+ );
1558
+ await this.upsertCreditNotes(
1559
+ [creditNote],
1560
+ accountId,
1561
+ false,
1562
+ this.getSyncTimestamp(event, refetched)
1563
+ );
1564
+ }
1565
+ async handleEarlyFraudWarningEvent(event, accountId) {
1566
+ const { entity: earlyFraudWarning, refetched } = await this.fetchOrUseWebhookData(
1567
+ event.data.object,
1568
+ (id) => this.stripe.radar.earlyFraudWarnings.retrieve(id)
1569
+ );
1570
+ await this.upsertEarlyFraudWarning(
1571
+ [earlyFraudWarning],
1572
+ accountId,
1573
+ false,
1574
+ this.getSyncTimestamp(event, refetched)
1575
+ );
1576
+ }
1577
+ async handleRefundEvent(event, accountId) {
1578
+ const { entity: refund, refetched } = await this.fetchOrUseWebhookData(
1579
+ event.data.object,
1580
+ (id) => this.stripe.refunds.retrieve(id)
1581
+ );
1582
+ await this.upsertRefunds([refund], accountId, false, this.getSyncTimestamp(event, refetched));
1583
+ }
1584
+ async handleReviewEvent(event, accountId) {
1585
+ const { entity: review, refetched } = await this.fetchOrUseWebhookData(
1586
+ event.data.object,
1587
+ (id) => this.stripe.reviews.retrieve(id)
1588
+ );
1589
+ await this.upsertReviews([review], accountId, false, this.getSyncTimestamp(event, refetched));
1590
+ }
1591
+ async handleEntitlementSummaryEvent(event, accountId) {
1592
+ const activeEntitlementSummary = event.data.object;
1593
+ let entitlements = activeEntitlementSummary.entitlements;
1594
+ let refetched = false;
1595
+ if (this.config.revalidateObjectsViaStripeApi?.includes("entitlements")) {
1596
+ const { lastResponse, ...rest } = await this.stripe.entitlements.activeEntitlements.list({
1597
+ customer: activeEntitlementSummary.customer
1598
+ });
1599
+ entitlements = rest;
1600
+ refetched = true;
1601
+ }
1602
+ await this.deleteRemovedActiveEntitlements(
1603
+ activeEntitlementSummary.customer,
1604
+ entitlements.data.map((entitlement) => entitlement.id)
1605
+ );
1606
+ await this.upsertActiveEntitlements(
1607
+ activeEntitlementSummary.customer,
1608
+ entitlements.data,
1609
+ accountId,
1610
+ false,
1611
+ this.getSyncTimestamp(event, refetched)
1612
+ );
1613
+ }
1497
1614
  getSyncTimestamp(event, refetched) {
1498
1615
  return refetched ? (/* @__PURE__ */ new Date()).toISOString() : new Date(event.created * 1e3).toISOString();
1499
1616
  }
@@ -1507,354 +1624,982 @@ var StripeSync = class {
1507
1624
  const fetchedEntity = await fetchFn(entity.id);
1508
1625
  return { entity: fetchedEntity, refetched: true };
1509
1626
  }
1510
- return { entity, refetched: false };
1627
+ return { entity, refetched: false };
1628
+ }
1629
+ async syncSingleEntity(stripeId) {
1630
+ const accountId = await this.getAccountId();
1631
+ if (stripeId.startsWith("cus_")) {
1632
+ return this.stripe.customers.retrieve(stripeId).then((it) => {
1633
+ if (!it || it.deleted) return;
1634
+ return this.upsertCustomers([it], accountId);
1635
+ });
1636
+ } else if (stripeId.startsWith("in_")) {
1637
+ return this.stripe.invoices.retrieve(stripeId).then((it) => this.upsertInvoices([it], accountId));
1638
+ } else if (stripeId.startsWith("price_")) {
1639
+ return this.stripe.prices.retrieve(stripeId).then((it) => this.upsertPrices([it], accountId));
1640
+ } else if (stripeId.startsWith("prod_")) {
1641
+ return this.stripe.products.retrieve(stripeId).then((it) => this.upsertProducts([it], accountId));
1642
+ } else if (stripeId.startsWith("sub_")) {
1643
+ return this.stripe.subscriptions.retrieve(stripeId).then((it) => this.upsertSubscriptions([it], accountId));
1644
+ } else if (stripeId.startsWith("seti_")) {
1645
+ return this.stripe.setupIntents.retrieve(stripeId).then((it) => this.upsertSetupIntents([it], accountId));
1646
+ } else if (stripeId.startsWith("pm_")) {
1647
+ return this.stripe.paymentMethods.retrieve(stripeId).then((it) => this.upsertPaymentMethods([it], accountId));
1648
+ } else if (stripeId.startsWith("dp_") || stripeId.startsWith("du_")) {
1649
+ return this.stripe.disputes.retrieve(stripeId).then((it) => this.upsertDisputes([it], accountId));
1650
+ } else if (stripeId.startsWith("ch_")) {
1651
+ return this.stripe.charges.retrieve(stripeId).then((it) => this.upsertCharges([it], accountId, true));
1652
+ } else if (stripeId.startsWith("pi_")) {
1653
+ return this.stripe.paymentIntents.retrieve(stripeId).then((it) => this.upsertPaymentIntents([it], accountId));
1654
+ } else if (stripeId.startsWith("txi_")) {
1655
+ return this.stripe.taxIds.retrieve(stripeId).then((it) => this.upsertTaxIds([it], accountId));
1656
+ } else if (stripeId.startsWith("cn_")) {
1657
+ return this.stripe.creditNotes.retrieve(stripeId).then((it) => this.upsertCreditNotes([it], accountId));
1658
+ } else if (stripeId.startsWith("issfr_")) {
1659
+ return this.stripe.radar.earlyFraudWarnings.retrieve(stripeId).then((it) => this.upsertEarlyFraudWarning([it], accountId));
1660
+ } else if (stripeId.startsWith("prv_")) {
1661
+ return this.stripe.reviews.retrieve(stripeId).then((it) => this.upsertReviews([it], accountId));
1662
+ } else if (stripeId.startsWith("re_")) {
1663
+ return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it], accountId));
1664
+ } else if (stripeId.startsWith("feat_")) {
1665
+ return this.stripe.entitlements.features.retrieve(stripeId).then((it) => this.upsertFeatures([it], accountId));
1666
+ } else if (stripeId.startsWith("cs_")) {
1667
+ return this.stripe.checkout.sessions.retrieve(stripeId).then((it) => this.upsertCheckoutSessions([it], accountId));
1668
+ }
1669
+ }
1670
+ /**
1671
+ * Process one page of items for the specified object type.
1672
+ * Returns the number of items processed and whether there are more pages.
1673
+ *
1674
+ * This method is designed for queue-based consumption where each page
1675
+ * is processed as a separate job. Uses the observable sync system for tracking.
1676
+ *
1677
+ * @param object - The Stripe object type to sync (e.g., 'customer', 'product')
1678
+ * @param params - Optional parameters for filtering and run context
1679
+ * @returns ProcessNextResult with processed count, hasMore flag, and runStartedAt
1680
+ *
1681
+ * @example
1682
+ * ```typescript
1683
+ * // Queue worker
1684
+ * const { hasMore, runStartedAt } = await stripeSync.processNext('customer')
1685
+ * if (hasMore) {
1686
+ * await queue.send({ object: 'customer', runStartedAt })
1687
+ * }
1688
+ * ```
1689
+ */
1690
+ async processNext(object, params) {
1691
+ await this.getCurrentAccount();
1692
+ const accountId = await this.getAccountId();
1693
+ const resourceName = this.getResourceName(object);
1694
+ let runStartedAt;
1695
+ if (params?.runStartedAt) {
1696
+ runStartedAt = params.runStartedAt;
1697
+ } else {
1698
+ const runKey = await this.postgresClient.getOrCreateSyncRun(
1699
+ accountId,
1700
+ params?.triggeredBy ?? "processNext"
1701
+ );
1702
+ if (!runKey) {
1703
+ const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1704
+ if (!activeRun) {
1705
+ throw new Error("Failed to get or create sync run");
1706
+ }
1707
+ runStartedAt = activeRun.runStartedAt;
1708
+ } else {
1709
+ runStartedAt = runKey.runStartedAt;
1710
+ }
1711
+ }
1712
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1713
+ const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
1714
+ if (objRun?.status === "complete" || objRun?.status === "error") {
1715
+ return {
1716
+ processed: 0,
1717
+ hasMore: false,
1718
+ runStartedAt
1719
+ };
1720
+ }
1721
+ if (objRun?.status === "pending") {
1722
+ const started = await this.postgresClient.tryStartObjectSync(
1723
+ accountId,
1724
+ runStartedAt,
1725
+ resourceName
1726
+ );
1727
+ if (!started) {
1728
+ return {
1729
+ processed: 0,
1730
+ hasMore: true,
1731
+ runStartedAt
1732
+ };
1733
+ }
1734
+ }
1735
+ let cursor = null;
1736
+ if (!params?.created) {
1737
+ if (objRun?.cursor) {
1738
+ cursor = parseInt(objRun.cursor);
1739
+ } else {
1740
+ const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
1741
+ cursor = lastCursor ? parseInt(lastCursor) : null;
1742
+ }
1743
+ }
1744
+ const result = await this.fetchOnePage(
1745
+ object,
1746
+ accountId,
1747
+ resourceName,
1748
+ runStartedAt,
1749
+ cursor,
1750
+ params
1751
+ );
1752
+ return result;
1753
+ }
1754
+ /**
1755
+ * Get the database resource name for a SyncObject type
1756
+ */
1757
+ getResourceName(object) {
1758
+ const mapping = {
1759
+ customer: "customers",
1760
+ invoice: "invoices",
1761
+ price: "prices",
1762
+ product: "products",
1763
+ subscription: "subscriptions",
1764
+ subscription_schedules: "subscription_schedules",
1765
+ setup_intent: "setup_intents",
1766
+ payment_method: "payment_methods",
1767
+ dispute: "disputes",
1768
+ charge: "charges",
1769
+ payment_intent: "payment_intents",
1770
+ plan: "plans",
1771
+ tax_id: "tax_ids",
1772
+ credit_note: "credit_notes",
1773
+ early_fraud_warning: "early_fraud_warnings",
1774
+ refund: "refunds",
1775
+ checkout_sessions: "checkout_sessions"
1776
+ };
1777
+ return mapping[object] || object;
1778
+ }
1779
+ /**
1780
+ * Fetch one page of items from Stripe and upsert to database.
1781
+ * Uses resourceRegistry for DRY list/upsert operations.
1782
+ * Uses the observable sync system for tracking progress.
1783
+ */
1784
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
1785
+ const limit = 100;
1786
+ if (object === "payment_method" || object === "tax_id") {
1787
+ this.config.logger?.warn(`processNext for ${object} requires customer context`);
1788
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
1789
+ return { processed: 0, hasMore: false, runStartedAt };
1790
+ }
1791
+ const config = this.resourceRegistry[object];
1792
+ if (!config) {
1793
+ throw new Error(`Unsupported object type for processNext: ${object}`);
1794
+ }
1795
+ try {
1796
+ const listParams = { limit };
1797
+ if (config.supportsCreatedFilter) {
1798
+ const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
1799
+ if (created) {
1800
+ listParams.created = created;
1801
+ }
1802
+ }
1803
+ const response = await config.listFn(listParams);
1804
+ if (response.data.length > 0) {
1805
+ this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
1806
+ await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
1807
+ await this.postgresClient.incrementObjectProgress(
1808
+ accountId,
1809
+ runStartedAt,
1810
+ resourceName,
1811
+ response.data.length
1812
+ );
1813
+ const maxCreated = Math.max(
1814
+ ...response.data.map((i) => i.created || 0)
1815
+ );
1816
+ if (maxCreated > 0) {
1817
+ await this.postgresClient.updateObjectCursor(
1818
+ accountId,
1819
+ runStartedAt,
1820
+ resourceName,
1821
+ String(maxCreated)
1822
+ );
1823
+ }
1824
+ }
1825
+ if (!response.has_more) {
1826
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
1827
+ }
1828
+ return {
1829
+ processed: response.data.length,
1830
+ hasMore: response.has_more,
1831
+ runStartedAt
1832
+ };
1833
+ } catch (error) {
1834
+ await this.postgresClient.failObjectSync(
1835
+ accountId,
1836
+ runStartedAt,
1837
+ resourceName,
1838
+ error instanceof Error ? error.message : "Unknown error"
1839
+ );
1840
+ throw error;
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Process all pages for all (or specified) object types until complete.
1845
+ *
1846
+ * @param params - Optional parameters for filtering and specifying object types
1847
+ * @returns SyncBackfill with counts for each synced resource type
1848
+ */
1849
+ /**
1850
+ * Process all pages for a single object type until complete.
1851
+ * Loops processNext() internally until hasMore is false.
1852
+ *
1853
+ * @param object - The object type to sync
1854
+ * @param runStartedAt - The sync run to use (for sharing across objects)
1855
+ * @param params - Optional sync parameters
1856
+ * @returns Sync result with count of synced items
1857
+ */
1858
+ async processObjectUntilDone(object, runStartedAt, params) {
1859
+ let totalSynced = 0;
1860
+ while (true) {
1861
+ const result = await this.processNext(object, {
1862
+ ...params,
1863
+ runStartedAt,
1864
+ triggeredBy: "processUntilDone"
1865
+ });
1866
+ totalSynced += result.processed;
1867
+ if (!result.hasMore) {
1868
+ break;
1869
+ }
1870
+ }
1871
+ return { synced: totalSynced };
1872
+ }
1873
+ async processUntilDone(params) {
1874
+ const { object } = params ?? { object: "all" };
1875
+ await this.getCurrentAccount();
1876
+ const accountId = await this.getAccountId();
1877
+ const runKey = await this.postgresClient.getOrCreateSyncRun(accountId, "processUntilDone");
1878
+ if (!runKey) {
1879
+ const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1880
+ if (!activeRun) {
1881
+ throw new Error("Failed to get or create sync run");
1882
+ }
1883
+ return this.processUntilDoneWithRun(activeRun.runStartedAt, object, params);
1884
+ }
1885
+ return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
1886
+ }
1887
+ /**
1888
+ * Internal implementation of processUntilDone with an existing run.
1889
+ */
1890
+ async processUntilDoneWithRun(runStartedAt, object, params) {
1891
+ const accountId = await this.getAccountId();
1892
+ const results = {};
1893
+ try {
1894
+ const objectsToSync = object === "all" || object === void 0 ? this.getSupportedSyncObjects() : [object];
1895
+ for (const obj of objectsToSync) {
1896
+ this.config.logger?.info(`Syncing ${obj}`);
1897
+ if (obj === "payment_method") {
1898
+ results.paymentMethods = await this.syncPaymentMethodsWithRun(runStartedAt, params);
1899
+ } else {
1900
+ const result = await this.processObjectUntilDone(obj, runStartedAt, params);
1901
+ switch (obj) {
1902
+ case "product":
1903
+ results.products = result;
1904
+ break;
1905
+ case "price":
1906
+ results.prices = result;
1907
+ break;
1908
+ case "plan":
1909
+ results.plans = result;
1910
+ break;
1911
+ case "customer":
1912
+ results.customers = result;
1913
+ break;
1914
+ case "subscription":
1915
+ results.subscriptions = result;
1916
+ break;
1917
+ case "subscription_schedules":
1918
+ results.subscriptionSchedules = result;
1919
+ break;
1920
+ case "invoice":
1921
+ results.invoices = result;
1922
+ break;
1923
+ case "charge":
1924
+ results.charges = result;
1925
+ break;
1926
+ case "setup_intent":
1927
+ results.setupIntents = result;
1928
+ break;
1929
+ case "payment_intent":
1930
+ results.paymentIntents = result;
1931
+ break;
1932
+ case "tax_id":
1933
+ results.taxIds = result;
1934
+ break;
1935
+ case "credit_note":
1936
+ results.creditNotes = result;
1937
+ break;
1938
+ case "dispute":
1939
+ results.disputes = result;
1940
+ break;
1941
+ case "early_fraud_warning":
1942
+ results.earlyFraudWarnings = result;
1943
+ break;
1944
+ case "refund":
1945
+ results.refunds = result;
1946
+ break;
1947
+ case "checkout_sessions":
1948
+ results.checkoutSessions = result;
1949
+ break;
1950
+ }
1951
+ }
1952
+ }
1953
+ await this.postgresClient.completeSyncRun(accountId, runStartedAt);
1954
+ return results;
1955
+ } catch (error) {
1956
+ await this.postgresClient.failSyncRun(
1957
+ accountId,
1958
+ runStartedAt,
1959
+ error instanceof Error ? error.message : "Unknown error"
1960
+ );
1961
+ throw error;
1962
+ }
1511
1963
  }
1512
- async syncSingleEntity(stripeId) {
1513
- if (stripeId.startsWith("cus_")) {
1514
- return this.stripe.customers.retrieve(stripeId).then((it) => {
1515
- if (!it || it.deleted) return;
1516
- return this.upsertCustomers([it]);
1517
- });
1518
- } else if (stripeId.startsWith("in_")) {
1519
- return this.stripe.invoices.retrieve(stripeId).then((it) => this.upsertInvoices([it]));
1520
- } else if (stripeId.startsWith("price_")) {
1521
- return this.stripe.prices.retrieve(stripeId).then((it) => this.upsertPrices([it]));
1522
- } else if (stripeId.startsWith("prod_")) {
1523
- return this.stripe.products.retrieve(stripeId).then((it) => this.upsertProducts([it]));
1524
- } else if (stripeId.startsWith("sub_")) {
1525
- return this.stripe.subscriptions.retrieve(stripeId).then((it) => this.upsertSubscriptions([it]));
1526
- } else if (stripeId.startsWith("seti_")) {
1527
- return this.stripe.setupIntents.retrieve(stripeId).then((it) => this.upsertSetupIntents([it]));
1528
- } else if (stripeId.startsWith("pm_")) {
1529
- return this.stripe.paymentMethods.retrieve(stripeId).then((it) => this.upsertPaymentMethods([it]));
1530
- } else if (stripeId.startsWith("dp_") || stripeId.startsWith("du_")) {
1531
- return this.stripe.disputes.retrieve(stripeId).then((it) => this.upsertDisputes([it]));
1532
- } else if (stripeId.startsWith("ch_")) {
1533
- return this.stripe.charges.retrieve(stripeId).then((it) => this.upsertCharges([it], true));
1534
- } else if (stripeId.startsWith("pi_")) {
1535
- return this.stripe.paymentIntents.retrieve(stripeId).then((it) => this.upsertPaymentIntents([it]));
1536
- } else if (stripeId.startsWith("txi_")) {
1537
- return this.stripe.taxIds.retrieve(stripeId).then((it) => this.upsertTaxIds([it]));
1538
- } else if (stripeId.startsWith("cn_")) {
1539
- return this.stripe.creditNotes.retrieve(stripeId).then((it) => this.upsertCreditNotes([it]));
1540
- } else if (stripeId.startsWith("issfr_")) {
1541
- return this.stripe.radar.earlyFraudWarnings.retrieve(stripeId).then((it) => this.upsertEarlyFraudWarning([it]));
1542
- } else if (stripeId.startsWith("prv_")) {
1543
- return this.stripe.reviews.retrieve(stripeId).then((it) => this.upsertReviews([it]));
1544
- } else if (stripeId.startsWith("re_")) {
1545
- return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it]));
1546
- } else if (stripeId.startsWith("feat_")) {
1547
- return this.stripe.entitlements.features.retrieve(stripeId).then((it) => this.upsertFeatures([it]));
1548
- } else if (stripeId.startsWith("cs_")) {
1549
- return this.stripe.checkout.sessions.retrieve(stripeId).then((it) => this.upsertCheckoutSessions([it]));
1550
- }
1551
- }
1552
- async syncBackfill(params) {
1553
- const { object } = params ?? {};
1554
- let products, prices, customers, checkoutSessions, subscriptions, subscriptionSchedules, invoices, setupIntents, paymentMethods, disputes, charges, paymentIntents, plans, taxIds, creditNotes, earlyFraudWarnings, refunds;
1555
- switch (object) {
1556
- case "all":
1557
- products = await this.syncProducts(params);
1558
- prices = await this.syncPrices(params);
1559
- plans = await this.syncPlans(params);
1560
- customers = await this.syncCustomers(params);
1561
- subscriptions = await this.syncSubscriptions(params);
1562
- subscriptionSchedules = await this.syncSubscriptionSchedules(params);
1563
- invoices = await this.syncInvoices(params);
1564
- charges = await this.syncCharges(params);
1565
- setupIntents = await this.syncSetupIntents(params);
1566
- paymentMethods = await this.syncPaymentMethods(params);
1567
- paymentIntents = await this.syncPaymentIntents(params);
1568
- taxIds = await this.syncTaxIds(params);
1569
- creditNotes = await this.syncCreditNotes(params);
1570
- disputes = await this.syncDisputes(params);
1571
- earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
1572
- refunds = await this.syncRefunds(params);
1573
- checkoutSessions = await this.syncCheckoutSessions(params);
1574
- break;
1575
- case "customer":
1576
- customers = await this.syncCustomers(params);
1577
- break;
1578
- case "invoice":
1579
- invoices = await this.syncInvoices(params);
1580
- break;
1581
- case "price":
1582
- prices = await this.syncPrices(params);
1583
- break;
1584
- case "product":
1585
- products = await this.syncProducts(params);
1586
- break;
1587
- case "subscription":
1588
- subscriptions = await this.syncSubscriptions(params);
1589
- break;
1590
- case "subscription_schedules":
1591
- subscriptionSchedules = await this.syncSubscriptionSchedules(params);
1592
- break;
1593
- case "setup_intent":
1594
- setupIntents = await this.syncSetupIntents(params);
1595
- break;
1596
- case "payment_method":
1597
- paymentMethods = await this.syncPaymentMethods(params);
1598
- break;
1599
- case "dispute":
1600
- disputes = await this.syncDisputes(params);
1601
- break;
1602
- case "charge":
1603
- charges = await this.syncCharges(params);
1604
- break;
1605
- case "payment_intent":
1606
- paymentIntents = await this.syncPaymentIntents(params);
1607
- case "plan":
1608
- plans = await this.syncPlans(params);
1609
- break;
1610
- case "tax_id":
1611
- taxIds = await this.syncTaxIds(params);
1612
- break;
1613
- case "credit_note":
1614
- creditNotes = await this.syncCreditNotes(params);
1615
- break;
1616
- case "early_fraud_warning":
1617
- earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
1618
- break;
1619
- case "refund":
1620
- refunds = await this.syncRefunds(params);
1621
- break;
1622
- case "checkout_sessions":
1623
- checkoutSessions = await this.syncCheckoutSessions(params);
1624
- break;
1625
- default:
1626
- break;
1964
+ /**
1965
+ * Sync payment methods with an existing run (special case - iterates customers)
1966
+ */
1967
+ async syncPaymentMethodsWithRun(runStartedAt, syncParams) {
1968
+ const accountId = await this.getAccountId();
1969
+ const resourceName = "payment_methods";
1970
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1971
+ await this.postgresClient.tryStartObjectSync(accountId, runStartedAt, resourceName);
1972
+ try {
1973
+ const prepared = (0, import_yesql2.pg)(
1974
+ `select id from "stripe"."customers" WHERE COALESCE(deleted, false) <> true;`
1975
+ )([]);
1976
+ const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
1977
+ this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
1978
+ let synced = 0;
1979
+ for (const customerIdChunk of chunkArray(customerIds, 10)) {
1980
+ await Promise.all(
1981
+ customerIdChunk.map(async (customerId) => {
1982
+ const CHECKPOINT_SIZE = 100;
1983
+ let currentBatch = [];
1984
+ for await (const item of this.stripe.paymentMethods.list({
1985
+ limit: 100,
1986
+ customer: customerId
1987
+ })) {
1988
+ currentBatch.push(item);
1989
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
1990
+ await this.upsertPaymentMethods(
1991
+ currentBatch,
1992
+ accountId,
1993
+ syncParams?.backfillRelatedEntities
1994
+ );
1995
+ synced += currentBatch.length;
1996
+ await this.postgresClient.incrementObjectProgress(
1997
+ accountId,
1998
+ runStartedAt,
1999
+ resourceName,
2000
+ currentBatch.length
2001
+ );
2002
+ currentBatch = [];
2003
+ }
2004
+ }
2005
+ if (currentBatch.length > 0) {
2006
+ await this.upsertPaymentMethods(
2007
+ currentBatch,
2008
+ accountId,
2009
+ syncParams?.backfillRelatedEntities
2010
+ );
2011
+ synced += currentBatch.length;
2012
+ await this.postgresClient.incrementObjectProgress(
2013
+ accountId,
2014
+ runStartedAt,
2015
+ resourceName,
2016
+ currentBatch.length
2017
+ );
2018
+ }
2019
+ })
2020
+ );
2021
+ }
2022
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2023
+ return { synced };
2024
+ } catch (error) {
2025
+ await this.postgresClient.failObjectSync(
2026
+ accountId,
2027
+ runStartedAt,
2028
+ resourceName,
2029
+ error instanceof Error ? error.message : "Unknown error"
2030
+ );
2031
+ throw error;
1627
2032
  }
1628
- return {
1629
- products,
1630
- prices,
1631
- customers,
1632
- checkoutSessions,
1633
- subscriptions,
1634
- subscriptionSchedules,
1635
- invoices,
1636
- setupIntents,
1637
- paymentMethods,
1638
- disputes,
1639
- charges,
1640
- paymentIntents,
1641
- plans,
1642
- taxIds,
1643
- creditNotes,
1644
- earlyFraudWarnings,
1645
- refunds
1646
- };
1647
2033
  }
1648
2034
  async syncProducts(syncParams) {
1649
2035
  this.config.logger?.info("Syncing products");
1650
- const params = { limit: 100 };
1651
- if (syncParams?.created) params.created = syncParams?.created;
1652
- return this.fetchAndUpsert(
1653
- () => this.stripe.products.list(params),
1654
- (products) => this.upsertProducts(products)
1655
- );
2036
+ return this.withSyncRun("products", "syncProducts", async (cursor, runStartedAt) => {
2037
+ const accountId = await this.getAccountId();
2038
+ const params = { limit: 100 };
2039
+ if (syncParams?.created) {
2040
+ params.created = syncParams.created;
2041
+ } else if (cursor) {
2042
+ params.created = { gte: cursor };
2043
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2044
+ }
2045
+ return this.fetchAndUpsert(
2046
+ () => this.stripe.products.list(params),
2047
+ (products) => this.upsertProducts(products, accountId),
2048
+ accountId,
2049
+ "products",
2050
+ runStartedAt
2051
+ );
2052
+ });
1656
2053
  }
1657
2054
  async syncPrices(syncParams) {
1658
2055
  this.config.logger?.info("Syncing prices");
1659
- const params = { limit: 100 };
1660
- if (syncParams?.created) params.created = syncParams?.created;
1661
- return this.fetchAndUpsert(
1662
- () => this.stripe.prices.list(params),
1663
- (prices) => this.upsertPrices(prices, syncParams?.backfillRelatedEntities)
1664
- );
2056
+ return this.withSyncRun("prices", "syncPrices", async (cursor, runStartedAt) => {
2057
+ const accountId = await this.getAccountId();
2058
+ const params = { limit: 100 };
2059
+ if (syncParams?.created) {
2060
+ params.created = syncParams.created;
2061
+ } else if (cursor) {
2062
+ params.created = { gte: cursor };
2063
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2064
+ }
2065
+ return this.fetchAndUpsert(
2066
+ () => this.stripe.prices.list(params),
2067
+ (prices) => this.upsertPrices(prices, accountId, syncParams?.backfillRelatedEntities),
2068
+ accountId,
2069
+ "prices",
2070
+ runStartedAt
2071
+ );
2072
+ });
1665
2073
  }
1666
2074
  async syncPlans(syncParams) {
1667
2075
  this.config.logger?.info("Syncing plans");
1668
- const params = { limit: 100 };
1669
- if (syncParams?.created) params.created = syncParams?.created;
1670
- return this.fetchAndUpsert(
1671
- () => this.stripe.plans.list(params),
1672
- (plans) => this.upsertPlans(plans, syncParams?.backfillRelatedEntities)
1673
- );
2076
+ return this.withSyncRun("plans", "syncPlans", async (cursor, runStartedAt) => {
2077
+ const accountId = await this.getAccountId();
2078
+ const params = { limit: 100 };
2079
+ if (syncParams?.created) {
2080
+ params.created = syncParams.created;
2081
+ } else if (cursor) {
2082
+ params.created = { gte: cursor };
2083
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2084
+ }
2085
+ return this.fetchAndUpsert(
2086
+ () => this.stripe.plans.list(params),
2087
+ (plans) => this.upsertPlans(plans, accountId, syncParams?.backfillRelatedEntities),
2088
+ accountId,
2089
+ "plans",
2090
+ runStartedAt
2091
+ );
2092
+ });
1674
2093
  }
1675
2094
  async syncCustomers(syncParams) {
1676
2095
  this.config.logger?.info("Syncing customers");
1677
- const params = { limit: 100 };
1678
- if (syncParams?.created) params.created = syncParams.created;
1679
- return this.fetchAndUpsert(
1680
- () => this.stripe.customers.list(params),
1681
- // @ts-expect-error
1682
- (items) => this.upsertCustomers(items)
1683
- );
2096
+ return this.withSyncRun("customers", "syncCustomers", async (cursor, runStartedAt) => {
2097
+ const accountId = await this.getAccountId();
2098
+ const params = { limit: 100 };
2099
+ if (syncParams?.created) {
2100
+ params.created = syncParams.created;
2101
+ } else if (cursor) {
2102
+ params.created = { gte: cursor };
2103
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2104
+ }
2105
+ return this.fetchAndUpsert(
2106
+ () => this.stripe.customers.list(params),
2107
+ // @ts-expect-error
2108
+ (items) => this.upsertCustomers(items, accountId),
2109
+ accountId,
2110
+ "customers",
2111
+ runStartedAt
2112
+ );
2113
+ });
1684
2114
  }
1685
2115
  async syncSubscriptions(syncParams) {
1686
2116
  this.config.logger?.info("Syncing subscriptions");
1687
- const params = { status: "all", limit: 100 };
1688
- if (syncParams?.created) params.created = syncParams.created;
1689
- return this.fetchAndUpsert(
1690
- () => this.stripe.subscriptions.list(params),
1691
- (items) => this.upsertSubscriptions(items, syncParams?.backfillRelatedEntities)
1692
- );
2117
+ return this.withSyncRun("subscriptions", "syncSubscriptions", async (cursor, runStartedAt) => {
2118
+ const accountId = await this.getAccountId();
2119
+ const params = { status: "all", limit: 100 };
2120
+ if (syncParams?.created) {
2121
+ params.created = syncParams.created;
2122
+ } else if (cursor) {
2123
+ params.created = { gte: cursor };
2124
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2125
+ }
2126
+ return this.fetchAndUpsert(
2127
+ () => this.stripe.subscriptions.list(params),
2128
+ (items) => this.upsertSubscriptions(items, accountId, syncParams?.backfillRelatedEntities),
2129
+ accountId,
2130
+ "subscriptions",
2131
+ runStartedAt
2132
+ );
2133
+ });
1693
2134
  }
1694
2135
  async syncSubscriptionSchedules(syncParams) {
1695
2136
  this.config.logger?.info("Syncing subscription schedules");
1696
- const params = { limit: 100 };
1697
- if (syncParams?.created) params.created = syncParams.created;
1698
- return this.fetchAndUpsert(
1699
- () => this.stripe.subscriptionSchedules.list(params),
1700
- (items) => this.upsertSubscriptionSchedules(items, syncParams?.backfillRelatedEntities)
2137
+ return this.withSyncRun(
2138
+ "subscription_schedules",
2139
+ "syncSubscriptionSchedules",
2140
+ async (cursor, runStartedAt) => {
2141
+ const accountId = await this.getAccountId();
2142
+ const params = { limit: 100 };
2143
+ if (syncParams?.created) {
2144
+ params.created = syncParams.created;
2145
+ } else if (cursor) {
2146
+ params.created = { gte: cursor };
2147
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2148
+ }
2149
+ return this.fetchAndUpsert(
2150
+ () => this.stripe.subscriptionSchedules.list(params),
2151
+ (items) => this.upsertSubscriptionSchedules(items, accountId, syncParams?.backfillRelatedEntities),
2152
+ accountId,
2153
+ "subscription_schedules",
2154
+ runStartedAt
2155
+ );
2156
+ }
1701
2157
  );
1702
2158
  }
1703
2159
  async syncInvoices(syncParams) {
1704
2160
  this.config.logger?.info("Syncing invoices");
1705
- const params = { limit: 100 };
1706
- if (syncParams?.created) params.created = syncParams.created;
1707
- return this.fetchAndUpsert(
1708
- () => this.stripe.invoices.list(params),
1709
- (items) => this.upsertInvoices(items, syncParams?.backfillRelatedEntities)
1710
- );
2161
+ return this.withSyncRun("invoices", "syncInvoices", async (cursor, runStartedAt) => {
2162
+ const accountId = await this.getAccountId();
2163
+ const params = { limit: 100 };
2164
+ if (syncParams?.created) {
2165
+ params.created = syncParams.created;
2166
+ } else if (cursor) {
2167
+ params.created = { gte: cursor };
2168
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2169
+ }
2170
+ return this.fetchAndUpsert(
2171
+ () => this.stripe.invoices.list(params),
2172
+ (items) => this.upsertInvoices(items, accountId, syncParams?.backfillRelatedEntities),
2173
+ accountId,
2174
+ "invoices",
2175
+ runStartedAt
2176
+ );
2177
+ });
1711
2178
  }
1712
2179
  async syncCharges(syncParams) {
1713
2180
  this.config.logger?.info("Syncing charges");
1714
- const params = { limit: 100 };
1715
- if (syncParams?.created) params.created = syncParams.created;
1716
- return this.fetchAndUpsert(
1717
- () => this.stripe.charges.list(params),
1718
- (items) => this.upsertCharges(items, syncParams?.backfillRelatedEntities)
1719
- );
2181
+ return this.withSyncRun("charges", "syncCharges", async (cursor, runStartedAt) => {
2182
+ const accountId = await this.getAccountId();
2183
+ const params = { limit: 100 };
2184
+ if (syncParams?.created) {
2185
+ params.created = syncParams.created;
2186
+ } else if (cursor) {
2187
+ params.created = { gte: cursor };
2188
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2189
+ }
2190
+ return this.fetchAndUpsert(
2191
+ () => this.stripe.charges.list(params),
2192
+ (items) => this.upsertCharges(items, accountId, syncParams?.backfillRelatedEntities),
2193
+ accountId,
2194
+ "charges",
2195
+ runStartedAt
2196
+ );
2197
+ });
1720
2198
  }
1721
2199
  async syncSetupIntents(syncParams) {
1722
2200
  this.config.logger?.info("Syncing setup_intents");
1723
- const params = { limit: 100 };
1724
- if (syncParams?.created) params.created = syncParams.created;
1725
- return this.fetchAndUpsert(
1726
- () => this.stripe.setupIntents.list(params),
1727
- (items) => this.upsertSetupIntents(items, syncParams?.backfillRelatedEntities)
1728
- );
2201
+ return this.withSyncRun("setup_intents", "syncSetupIntents", async (cursor, runStartedAt) => {
2202
+ const accountId = await this.getAccountId();
2203
+ const params = { limit: 100 };
2204
+ if (syncParams?.created) {
2205
+ params.created = syncParams.created;
2206
+ } else if (cursor) {
2207
+ params.created = { gte: cursor };
2208
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2209
+ }
2210
+ return this.fetchAndUpsert(
2211
+ () => this.stripe.setupIntents.list(params),
2212
+ (items) => this.upsertSetupIntents(items, accountId, syncParams?.backfillRelatedEntities),
2213
+ accountId,
2214
+ "setup_intents",
2215
+ runStartedAt
2216
+ );
2217
+ });
1729
2218
  }
1730
2219
  async syncPaymentIntents(syncParams) {
1731
2220
  this.config.logger?.info("Syncing payment_intents");
1732
- const params = { limit: 100 };
1733
- if (syncParams?.created) params.created = syncParams.created;
1734
- return this.fetchAndUpsert(
1735
- () => this.stripe.paymentIntents.list(params),
1736
- (items) => this.upsertPaymentIntents(items, syncParams?.backfillRelatedEntities)
2221
+ return this.withSyncRun(
2222
+ "payment_intents",
2223
+ "syncPaymentIntents",
2224
+ async (cursor, runStartedAt) => {
2225
+ const accountId = await this.getAccountId();
2226
+ const params = { limit: 100 };
2227
+ if (syncParams?.created) {
2228
+ params.created = syncParams.created;
2229
+ } else if (cursor) {
2230
+ params.created = { gte: cursor };
2231
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2232
+ }
2233
+ return this.fetchAndUpsert(
2234
+ () => this.stripe.paymentIntents.list(params),
2235
+ (items) => this.upsertPaymentIntents(items, accountId, syncParams?.backfillRelatedEntities),
2236
+ accountId,
2237
+ "payment_intents",
2238
+ runStartedAt
2239
+ );
2240
+ }
1737
2241
  );
1738
2242
  }
1739
2243
  async syncTaxIds(syncParams) {
1740
2244
  this.config.logger?.info("Syncing tax_ids");
1741
- const params = { limit: 100 };
1742
- return this.fetchAndUpsert(
1743
- () => this.stripe.taxIds.list(params),
1744
- (items) => this.upsertTaxIds(items, syncParams?.backfillRelatedEntities)
1745
- );
2245
+ return this.withSyncRun("tax_ids", "syncTaxIds", async (_cursor, runStartedAt) => {
2246
+ const accountId = await this.getAccountId();
2247
+ const params = { limit: 100 };
2248
+ return this.fetchAndUpsert(
2249
+ () => this.stripe.taxIds.list(params),
2250
+ (items) => this.upsertTaxIds(items, accountId, syncParams?.backfillRelatedEntities),
2251
+ accountId,
2252
+ "tax_ids",
2253
+ runStartedAt
2254
+ );
2255
+ });
1746
2256
  }
1747
2257
  async syncPaymentMethods(syncParams) {
1748
2258
  this.config.logger?.info("Syncing payment method");
1749
- const prepared = (0, import_yesql2.pg)(
1750
- `select id from "${this.config.schema}"."customers" WHERE deleted <> true;`
1751
- )([]);
1752
- const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
1753
- this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
1754
- let synced = 0;
1755
- for (const customerIdChunk of chunkArray(customerIds, 10)) {
1756
- await Promise.all(
1757
- customerIdChunk.map(async (customerId) => {
1758
- const syncResult = await this.fetchAndUpsert(
1759
- () => this.stripe.paymentMethods.list({
1760
- limit: 100,
1761
- customer: customerId
1762
- }),
1763
- (items) => this.upsertPaymentMethods(items, syncParams?.backfillRelatedEntities)
2259
+ return this.withSyncRun(
2260
+ "payment_methods",
2261
+ "syncPaymentMethods",
2262
+ async (_cursor, runStartedAt) => {
2263
+ const accountId = await this.getAccountId();
2264
+ const prepared = (0, import_yesql2.pg)(
2265
+ `select id from "stripe"."customers" WHERE COALESCE(deleted, false) <> true;`
2266
+ )([]);
2267
+ const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
2268
+ this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
2269
+ let synced = 0;
2270
+ for (const customerIdChunk of chunkArray(customerIds, 10)) {
2271
+ await Promise.all(
2272
+ customerIdChunk.map(async (customerId) => {
2273
+ const CHECKPOINT_SIZE = 100;
2274
+ let currentBatch = [];
2275
+ for await (const item of this.stripe.paymentMethods.list({
2276
+ limit: 100,
2277
+ customer: customerId
2278
+ })) {
2279
+ currentBatch.push(item);
2280
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2281
+ await this.upsertPaymentMethods(
2282
+ currentBatch,
2283
+ accountId,
2284
+ syncParams?.backfillRelatedEntities
2285
+ );
2286
+ synced += currentBatch.length;
2287
+ await this.postgresClient.incrementObjectProgress(
2288
+ accountId,
2289
+ runStartedAt,
2290
+ "payment_methods",
2291
+ currentBatch.length
2292
+ );
2293
+ currentBatch = [];
2294
+ }
2295
+ }
2296
+ if (currentBatch.length > 0) {
2297
+ await this.upsertPaymentMethods(
2298
+ currentBatch,
2299
+ accountId,
2300
+ syncParams?.backfillRelatedEntities
2301
+ );
2302
+ synced += currentBatch.length;
2303
+ await this.postgresClient.incrementObjectProgress(
2304
+ accountId,
2305
+ runStartedAt,
2306
+ "payment_methods",
2307
+ currentBatch.length
2308
+ );
2309
+ }
2310
+ })
1764
2311
  );
1765
- synced += syncResult.synced;
1766
- })
1767
- );
1768
- }
1769
- return { synced };
2312
+ }
2313
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, "payment_methods");
2314
+ return { synced };
2315
+ }
2316
+ );
1770
2317
  }
1771
2318
  async syncDisputes(syncParams) {
1772
- const params = { limit: 100 };
1773
- if (syncParams?.created) params.created = syncParams.created;
1774
- return this.fetchAndUpsert(
1775
- () => this.stripe.disputes.list(params),
1776
- (items) => this.upsertDisputes(items, syncParams?.backfillRelatedEntities)
1777
- );
2319
+ this.config.logger?.info("Syncing disputes");
2320
+ return this.withSyncRun("disputes", "syncDisputes", async (cursor, runStartedAt) => {
2321
+ const accountId = await this.getAccountId();
2322
+ const params = { limit: 100 };
2323
+ if (syncParams?.created) {
2324
+ params.created = syncParams.created;
2325
+ } else if (cursor) {
2326
+ params.created = { gte: cursor };
2327
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2328
+ }
2329
+ return this.fetchAndUpsert(
2330
+ () => this.stripe.disputes.list(params),
2331
+ (items) => this.upsertDisputes(items, accountId, syncParams?.backfillRelatedEntities),
2332
+ accountId,
2333
+ "disputes",
2334
+ runStartedAt
2335
+ );
2336
+ });
1778
2337
  }
1779
2338
  async syncEarlyFraudWarnings(syncParams) {
1780
2339
  this.config.logger?.info("Syncing early fraud warnings");
1781
- const params = { limit: 100 };
1782
- if (syncParams?.created) params.created = syncParams.created;
1783
- return this.fetchAndUpsert(
1784
- () => this.stripe.radar.earlyFraudWarnings.list(params),
1785
- (items) => this.upsertEarlyFraudWarning(items, syncParams?.backfillRelatedEntities)
2340
+ return this.withSyncRun(
2341
+ "early_fraud_warnings",
2342
+ "syncEarlyFraudWarnings",
2343
+ async (cursor, runStartedAt) => {
2344
+ const accountId = await this.getAccountId();
2345
+ const params = { limit: 100 };
2346
+ if (syncParams?.created) {
2347
+ params.created = syncParams.created;
2348
+ } else if (cursor) {
2349
+ params.created = { gte: cursor };
2350
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2351
+ }
2352
+ return this.fetchAndUpsert(
2353
+ () => this.stripe.radar.earlyFraudWarnings.list(params),
2354
+ (items) => this.upsertEarlyFraudWarning(items, accountId, syncParams?.backfillRelatedEntities),
2355
+ accountId,
2356
+ "early_fraud_warnings",
2357
+ runStartedAt
2358
+ );
2359
+ }
1786
2360
  );
1787
2361
  }
1788
2362
  async syncRefunds(syncParams) {
1789
2363
  this.config.logger?.info("Syncing refunds");
1790
- const params = { limit: 100 };
1791
- if (syncParams?.created) params.created = syncParams.created;
1792
- return this.fetchAndUpsert(
1793
- () => this.stripe.refunds.list(params),
1794
- (items) => this.upsertRefunds(items, syncParams?.backfillRelatedEntities)
1795
- );
2364
+ return this.withSyncRun("refunds", "syncRefunds", async (cursor, runStartedAt) => {
2365
+ const accountId = await this.getAccountId();
2366
+ const params = { limit: 100 };
2367
+ if (syncParams?.created) {
2368
+ params.created = syncParams.created;
2369
+ } else if (cursor) {
2370
+ params.created = { gte: cursor };
2371
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2372
+ }
2373
+ return this.fetchAndUpsert(
2374
+ () => this.stripe.refunds.list(params),
2375
+ (items) => this.upsertRefunds(items, accountId, syncParams?.backfillRelatedEntities),
2376
+ accountId,
2377
+ "refunds",
2378
+ runStartedAt
2379
+ );
2380
+ });
1796
2381
  }
1797
2382
  async syncCreditNotes(syncParams) {
1798
2383
  this.config.logger?.info("Syncing credit notes");
1799
- const params = { limit: 100 };
1800
- if (syncParams?.created) params.created = syncParams?.created;
1801
- return this.fetchAndUpsert(
1802
- () => this.stripe.creditNotes.list(params),
1803
- (creditNotes) => this.upsertCreditNotes(creditNotes)
1804
- );
2384
+ return this.withSyncRun("credit_notes", "syncCreditNotes", async (cursor, runStartedAt) => {
2385
+ const accountId = await this.getAccountId();
2386
+ const params = { limit: 100 };
2387
+ if (syncParams?.created) {
2388
+ params.created = syncParams.created;
2389
+ } else if (cursor) {
2390
+ params.created = { gte: cursor };
2391
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2392
+ }
2393
+ return this.fetchAndUpsert(
2394
+ () => this.stripe.creditNotes.list(params),
2395
+ (creditNotes) => this.upsertCreditNotes(creditNotes, accountId),
2396
+ accountId,
2397
+ "credit_notes",
2398
+ runStartedAt
2399
+ );
2400
+ });
1805
2401
  }
1806
2402
  async syncFeatures(syncParams) {
1807
2403
  this.config.logger?.info("Syncing features");
1808
- const params = { limit: 100, ...syncParams?.pagination };
1809
- return this.fetchAndUpsert(
1810
- () => this.stripe.entitlements.features.list(params),
1811
- (features) => this.upsertFeatures(features)
1812
- );
2404
+ return this.withSyncRun("features", "syncFeatures", async (cursor, runStartedAt) => {
2405
+ const accountId = await this.getAccountId();
2406
+ const params = {
2407
+ limit: 100,
2408
+ ...syncParams?.pagination
2409
+ };
2410
+ return this.fetchAndUpsert(
2411
+ () => this.stripe.entitlements.features.list(params),
2412
+ (features) => this.upsertFeatures(features, accountId),
2413
+ accountId,
2414
+ "features",
2415
+ runStartedAt
2416
+ );
2417
+ });
1813
2418
  }
1814
2419
  async syncEntitlements(customerId, syncParams) {
1815
2420
  this.config.logger?.info("Syncing entitlements");
1816
- const params = {
1817
- customer: customerId,
1818
- limit: 100,
1819
- ...syncParams?.pagination
1820
- };
1821
- return this.fetchAndUpsert(
1822
- () => this.stripe.entitlements.activeEntitlements.list(params),
1823
- (entitlements) => this.upsertActiveEntitlements(customerId, entitlements)
2421
+ return this.withSyncRun(
2422
+ "active_entitlements",
2423
+ "syncEntitlements",
2424
+ async (cursor, runStartedAt) => {
2425
+ const accountId = await this.getAccountId();
2426
+ const params = {
2427
+ customer: customerId,
2428
+ limit: 100,
2429
+ ...syncParams?.pagination
2430
+ };
2431
+ return this.fetchAndUpsert(
2432
+ () => this.stripe.entitlements.activeEntitlements.list(params),
2433
+ (entitlements) => this.upsertActiveEntitlements(customerId, entitlements, accountId),
2434
+ accountId,
2435
+ "active_entitlements",
2436
+ runStartedAt
2437
+ );
2438
+ }
1824
2439
  );
1825
2440
  }
1826
2441
  async syncCheckoutSessions(syncParams) {
1827
2442
  this.config.logger?.info("Syncing checkout sessions");
1828
- const params = {
1829
- limit: 100
1830
- };
1831
- if (syncParams?.created) params.created = syncParams.created;
1832
- return this.fetchAndUpsert(
1833
- () => this.stripe.checkout.sessions.list(params),
1834
- (items) => this.upsertCheckoutSessions(items, syncParams?.backfillRelatedEntities)
2443
+ return this.withSyncRun(
2444
+ "checkout_sessions",
2445
+ "syncCheckoutSessions",
2446
+ async (cursor, runStartedAt) => {
2447
+ const accountId = await this.getAccountId();
2448
+ const params = { limit: 100 };
2449
+ if (syncParams?.created) {
2450
+ params.created = syncParams.created;
2451
+ } else if (cursor) {
2452
+ params.created = { gte: cursor };
2453
+ this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2454
+ }
2455
+ return this.fetchAndUpsert(
2456
+ () => this.stripe.checkout.sessions.list(params),
2457
+ (items) => this.upsertCheckoutSessions(items, accountId, syncParams?.backfillRelatedEntities),
2458
+ accountId,
2459
+ "checkout_sessions",
2460
+ runStartedAt
2461
+ );
2462
+ }
1835
2463
  );
1836
2464
  }
1837
- async fetchAndUpsert(fetch, upsert) {
1838
- const items = [];
1839
- this.config.logger?.info("Fetching items to sync from Stripe");
1840
- for await (const item of fetch()) {
1841
- items.push(item);
2465
+ /**
2466
+ * Helper to wrap a sync operation in the observable sync system.
2467
+ * Creates/gets a sync run, sets up the object run, gets cursor, and handles completion.
2468
+ *
2469
+ * @param resourceName - The resource being synced (e.g., 'products', 'customers')
2470
+ * @param triggeredBy - What triggered this sync (for observability)
2471
+ * @param fn - The sync function to execute, receives cursor and runStartedAt
2472
+ * @returns The result of the sync function
2473
+ */
2474
+ async withSyncRun(resourceName, triggeredBy, fn) {
2475
+ const accountId = await this.getAccountId();
2476
+ const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
2477
+ const cursor = lastCursor ? parseInt(lastCursor) : null;
2478
+ const runKey = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
2479
+ if (!runKey) {
2480
+ const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
2481
+ if (!activeRun) {
2482
+ throw new Error("Failed to get or create sync run");
2483
+ }
2484
+ throw new Error("Another sync is already running for this account");
2485
+ }
2486
+ const { runStartedAt } = runKey;
2487
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2488
+ await this.postgresClient.tryStartObjectSync(accountId, runStartedAt, resourceName);
2489
+ try {
2490
+ const result = await fn(cursor, runStartedAt);
2491
+ await this.postgresClient.completeSyncRun(accountId, runStartedAt);
2492
+ return result;
2493
+ } catch (error) {
2494
+ await this.postgresClient.failSyncRun(
2495
+ accountId,
2496
+ runStartedAt,
2497
+ error instanceof Error ? error.message : "Unknown error"
2498
+ );
2499
+ throw error;
1842
2500
  }
1843
- if (!items.length) return { synced: 0 };
1844
- this.config.logger?.info(`Upserting ${items.length} items`);
1845
- const chunkSize = 250;
1846
- for (let i = 0; i < items.length; i += chunkSize) {
1847
- const chunk = items.slice(i, i + chunkSize);
1848
- await upsert(chunk);
2501
+ }
2502
+ async fetchAndUpsert(fetch, upsert, accountId, resourceName, runStartedAt) {
2503
+ const CHECKPOINT_SIZE = 100;
2504
+ let totalSynced = 0;
2505
+ let currentBatch = [];
2506
+ try {
2507
+ this.config.logger?.info("Fetching items to sync from Stripe");
2508
+ try {
2509
+ for await (const item of fetch()) {
2510
+ currentBatch.push(item);
2511
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2512
+ this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
2513
+ await upsert(currentBatch, accountId);
2514
+ totalSynced += currentBatch.length;
2515
+ await this.postgresClient.incrementObjectProgress(
2516
+ accountId,
2517
+ runStartedAt,
2518
+ resourceName,
2519
+ currentBatch.length
2520
+ );
2521
+ const maxCreated = Math.max(
2522
+ ...currentBatch.map((i) => i.created || 0)
2523
+ );
2524
+ if (maxCreated > 0) {
2525
+ await this.postgresClient.updateObjectCursor(
2526
+ accountId,
2527
+ runStartedAt,
2528
+ resourceName,
2529
+ String(maxCreated)
2530
+ );
2531
+ this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
2532
+ }
2533
+ currentBatch = [];
2534
+ }
2535
+ }
2536
+ if (currentBatch.length > 0) {
2537
+ this.config.logger?.info(`Upserting final batch of ${currentBatch.length} items`);
2538
+ await upsert(currentBatch, accountId);
2539
+ totalSynced += currentBatch.length;
2540
+ await this.postgresClient.incrementObjectProgress(
2541
+ accountId,
2542
+ runStartedAt,
2543
+ resourceName,
2544
+ currentBatch.length
2545
+ );
2546
+ const maxCreated = Math.max(
2547
+ ...currentBatch.map((i) => i.created || 0)
2548
+ );
2549
+ if (maxCreated > 0) {
2550
+ await this.postgresClient.updateObjectCursor(
2551
+ accountId,
2552
+ runStartedAt,
2553
+ resourceName,
2554
+ String(maxCreated)
2555
+ );
2556
+ }
2557
+ }
2558
+ } catch (error) {
2559
+ if (currentBatch.length > 0) {
2560
+ this.config.logger?.info(
2561
+ `Error occurred, saving partial progress: ${currentBatch.length} items`
2562
+ );
2563
+ await upsert(currentBatch, accountId);
2564
+ totalSynced += currentBatch.length;
2565
+ await this.postgresClient.incrementObjectProgress(
2566
+ accountId,
2567
+ runStartedAt,
2568
+ resourceName,
2569
+ currentBatch.length
2570
+ );
2571
+ const maxCreated = Math.max(
2572
+ ...currentBatch.map((i) => i.created || 0)
2573
+ );
2574
+ if (maxCreated > 0) {
2575
+ await this.postgresClient.updateObjectCursor(
2576
+ accountId,
2577
+ runStartedAt,
2578
+ resourceName,
2579
+ String(maxCreated)
2580
+ );
2581
+ }
2582
+ }
2583
+ throw error;
2584
+ }
2585
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2586
+ this.config.logger?.info(`Sync complete: ${totalSynced} items synced`);
2587
+ return { synced: totalSynced };
2588
+ } catch (error) {
2589
+ await this.postgresClient.failObjectSync(
2590
+ accountId,
2591
+ runStartedAt,
2592
+ resourceName,
2593
+ error instanceof Error ? error.message : "Unknown error"
2594
+ );
2595
+ throw error;
1849
2596
  }
1850
- this.config.logger?.info("Upserted items");
1851
- return { synced: items.length };
1852
2597
  }
1853
- async upsertCharges(charges, backfillRelatedEntities, syncTimestamp) {
2598
+ async upsertCharges(charges, accountId, backfillRelatedEntities, syncTimestamp) {
1854
2599
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1855
2600
  await Promise.all([
1856
- this.backfillCustomers(getUniqueIds(charges, "customer")),
1857
- this.backfillInvoices(getUniqueIds(charges, "invoice"))
2601
+ this.backfillCustomers(getUniqueIds(charges, "customer"), accountId),
2602
+ this.backfillInvoices(getUniqueIds(charges, "invoice"), accountId)
1858
2603
  ]);
1859
2604
  }
1860
2605
  await this.expandEntity(
@@ -1865,18 +2610,18 @@ var StripeSync = class {
1865
2610
  return this.postgresClient.upsertManyWithTimestampProtection(
1866
2611
  charges,
1867
2612
  "charges",
1868
- chargeSchema,
2613
+ accountId,
1869
2614
  syncTimestamp
1870
2615
  );
1871
2616
  }
1872
- async backfillCharges(chargeIds) {
2617
+ async backfillCharges(chargeIds, accountId) {
1873
2618
  const missingChargeIds = await this.postgresClient.findMissingEntries("charges", chargeIds);
1874
2619
  await this.fetchMissingEntities(
1875
2620
  missingChargeIds,
1876
2621
  (id) => this.stripe.charges.retrieve(id)
1877
- ).then((charges) => this.upsertCharges(charges));
2622
+ ).then((charges) => this.upsertCharges(charges, accountId));
1878
2623
  }
1879
- async backfillPaymentIntents(paymentIntentIds) {
2624
+ async backfillPaymentIntents(paymentIntentIds, accountId) {
1880
2625
  const missingIds = await this.postgresClient.findMissingEntries(
1881
2626
  "payment_intents",
1882
2627
  paymentIntentIds
@@ -1884,13 +2629,13 @@ var StripeSync = class {
1884
2629
  await this.fetchMissingEntities(
1885
2630
  missingIds,
1886
2631
  (id) => this.stripe.paymentIntents.retrieve(id)
1887
- ).then((paymentIntents) => this.upsertPaymentIntents(paymentIntents));
2632
+ ).then((paymentIntents) => this.upsertPaymentIntents(paymentIntents, accountId));
1888
2633
  }
1889
- async upsertCreditNotes(creditNotes, backfillRelatedEntities, syncTimestamp) {
2634
+ async upsertCreditNotes(creditNotes, accountId, backfillRelatedEntities, syncTimestamp) {
1890
2635
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1891
2636
  await Promise.all([
1892
- this.backfillCustomers(getUniqueIds(creditNotes, "customer")),
1893
- this.backfillInvoices(getUniqueIds(creditNotes, "invoice"))
2637
+ this.backfillCustomers(getUniqueIds(creditNotes, "customer"), accountId),
2638
+ this.backfillInvoices(getUniqueIds(creditNotes, "invoice"), accountId)
1894
2639
  ]);
1895
2640
  }
1896
2641
  await this.expandEntity(
@@ -1901,113 +2646,114 @@ var StripeSync = class {
1901
2646
  return this.postgresClient.upsertManyWithTimestampProtection(
1902
2647
  creditNotes,
1903
2648
  "credit_notes",
1904
- creditNoteSchema,
2649
+ accountId,
1905
2650
  syncTimestamp
1906
2651
  );
1907
2652
  }
1908
- async upsertCheckoutSessions(checkoutSessions, backfillRelatedEntities, syncTimestamp) {
2653
+ async upsertCheckoutSessions(checkoutSessions, accountId, backfillRelatedEntities, syncTimestamp) {
1909
2654
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1910
2655
  await Promise.all([
1911
- this.backfillCustomers(getUniqueIds(checkoutSessions, "customer")),
1912
- this.backfillSubscriptions(getUniqueIds(checkoutSessions, "subscription")),
1913
- this.backfillPaymentIntents(getUniqueIds(checkoutSessions, "payment_intent")),
1914
- this.backfillInvoices(getUniqueIds(checkoutSessions, "invoice"))
2656
+ this.backfillCustomers(getUniqueIds(checkoutSessions, "customer"), accountId),
2657
+ this.backfillSubscriptions(getUniqueIds(checkoutSessions, "subscription"), accountId),
2658
+ this.backfillPaymentIntents(getUniqueIds(checkoutSessions, "payment_intent"), accountId),
2659
+ this.backfillInvoices(getUniqueIds(checkoutSessions, "invoice"), accountId)
1915
2660
  ]);
1916
2661
  }
1917
2662
  const rows = await this.postgresClient.upsertManyWithTimestampProtection(
1918
2663
  checkoutSessions,
1919
2664
  "checkout_sessions",
1920
- checkoutSessionSchema,
2665
+ accountId,
1921
2666
  syncTimestamp
1922
2667
  );
1923
2668
  await this.fillCheckoutSessionsLineItems(
1924
2669
  checkoutSessions.map((cs) => cs.id),
2670
+ accountId,
1925
2671
  syncTimestamp
1926
2672
  );
1927
2673
  return rows;
1928
2674
  }
1929
- async upsertEarlyFraudWarning(earlyFraudWarnings, backfillRelatedEntities, syncTimestamp) {
2675
+ async upsertEarlyFraudWarning(earlyFraudWarnings, accountId, backfillRelatedEntities, syncTimestamp) {
1930
2676
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1931
2677
  await Promise.all([
1932
- this.backfillPaymentIntents(getUniqueIds(earlyFraudWarnings, "payment_intent")),
1933
- this.backfillCharges(getUniqueIds(earlyFraudWarnings, "charge"))
2678
+ this.backfillPaymentIntents(getUniqueIds(earlyFraudWarnings, "payment_intent"), accountId),
2679
+ this.backfillCharges(getUniqueIds(earlyFraudWarnings, "charge"), accountId)
1934
2680
  ]);
1935
2681
  }
1936
2682
  return this.postgresClient.upsertManyWithTimestampProtection(
1937
2683
  earlyFraudWarnings,
1938
2684
  "early_fraud_warnings",
1939
- earlyFraudWarningSchema,
2685
+ accountId,
1940
2686
  syncTimestamp
1941
2687
  );
1942
2688
  }
1943
- async upsertRefunds(refunds, backfillRelatedEntities, syncTimestamp) {
2689
+ async upsertRefunds(refunds, accountId, backfillRelatedEntities, syncTimestamp) {
1944
2690
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1945
2691
  await Promise.all([
1946
- this.backfillPaymentIntents(getUniqueIds(refunds, "payment_intent")),
1947
- this.backfillCharges(getUniqueIds(refunds, "charge"))
2692
+ this.backfillPaymentIntents(getUniqueIds(refunds, "payment_intent"), accountId),
2693
+ this.backfillCharges(getUniqueIds(refunds, "charge"), accountId)
1948
2694
  ]);
1949
2695
  }
1950
2696
  return this.postgresClient.upsertManyWithTimestampProtection(
1951
2697
  refunds,
1952
2698
  "refunds",
1953
- refundSchema,
2699
+ accountId,
1954
2700
  syncTimestamp
1955
2701
  );
1956
2702
  }
1957
- async upsertReviews(reviews, backfillRelatedEntities, syncTimestamp) {
2703
+ async upsertReviews(reviews, accountId, backfillRelatedEntities, syncTimestamp) {
1958
2704
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1959
2705
  await Promise.all([
1960
- this.backfillPaymentIntents(getUniqueIds(reviews, "payment_intent")),
1961
- this.backfillCharges(getUniqueIds(reviews, "charge"))
2706
+ this.backfillPaymentIntents(getUniqueIds(reviews, "payment_intent"), accountId),
2707
+ this.backfillCharges(getUniqueIds(reviews, "charge"), accountId)
1962
2708
  ]);
1963
2709
  }
1964
2710
  return this.postgresClient.upsertManyWithTimestampProtection(
1965
2711
  reviews,
1966
2712
  "reviews",
1967
- reviewSchema,
2713
+ accountId,
1968
2714
  syncTimestamp
1969
2715
  );
1970
2716
  }
1971
- async upsertCustomers(customers, syncTimestamp) {
2717
+ async upsertCustomers(customers, accountId, syncTimestamp) {
1972
2718
  const deletedCustomers = customers.filter((customer) => customer.deleted);
1973
2719
  const nonDeletedCustomers = customers.filter((customer) => !customer.deleted);
1974
2720
  await this.postgresClient.upsertManyWithTimestampProtection(
1975
2721
  nonDeletedCustomers,
1976
2722
  "customers",
1977
- customerSchema,
2723
+ accountId,
1978
2724
  syncTimestamp
1979
2725
  );
1980
2726
  await this.postgresClient.upsertManyWithTimestampProtection(
1981
2727
  deletedCustomers,
1982
2728
  "customers",
1983
- customerDeletedSchema,
2729
+ accountId,
1984
2730
  syncTimestamp
1985
2731
  );
1986
2732
  return customers;
1987
2733
  }
1988
- async backfillCustomers(customerIds) {
2734
+ async backfillCustomers(customerIds, accountId) {
1989
2735
  const missingIds = await this.postgresClient.findMissingEntries("customers", customerIds);
1990
- await this.fetchMissingEntities(missingIds, (id) => this.stripe.customers.retrieve(id)).then((entries) => this.upsertCustomers(entries)).catch((err) => {
2736
+ await this.fetchMissingEntities(missingIds, (id) => this.stripe.customers.retrieve(id)).then((entries) => this.upsertCustomers(entries, accountId)).catch((err) => {
1991
2737
  this.config.logger?.error(err, "Failed to backfill");
1992
2738
  throw err;
1993
2739
  });
1994
2740
  }
1995
- async upsertDisputes(disputes, backfillRelatedEntities, syncTimestamp) {
2741
+ async upsertDisputes(disputes, accountId, backfillRelatedEntities, syncTimestamp) {
1996
2742
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1997
- await this.backfillCharges(getUniqueIds(disputes, "charge"));
2743
+ await this.backfillCharges(getUniqueIds(disputes, "charge"), accountId);
1998
2744
  }
1999
2745
  return this.postgresClient.upsertManyWithTimestampProtection(
2000
2746
  disputes,
2001
2747
  "disputes",
2002
- disputeSchema,
2748
+ accountId,
2003
2749
  syncTimestamp
2004
2750
  );
2005
2751
  }
2006
- async upsertInvoices(invoices, backfillRelatedEntities, syncTimestamp) {
2752
+ async upsertInvoices(invoices, accountId, backfillRelatedEntities, syncTimestamp) {
2007
2753
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2008
2754
  await Promise.all([
2009
- this.backfillCustomers(getUniqueIds(invoices, "customer")),
2010
- this.backfillSubscriptions(getUniqueIds(invoices, "subscription"))
2755
+ this.backfillCustomers(getUniqueIds(invoices, "customer"), accountId),
2756
+ this.backfillSubscriptions(getUniqueIds(invoices, "subscription"), accountId)
2011
2757
  ]);
2012
2758
  }
2013
2759
  await this.expandEntity(
@@ -2018,119 +2764,119 @@ var StripeSync = class {
2018
2764
  return this.postgresClient.upsertManyWithTimestampProtection(
2019
2765
  invoices,
2020
2766
  "invoices",
2021
- invoiceSchema,
2767
+ accountId,
2022
2768
  syncTimestamp
2023
2769
  );
2024
2770
  }
2025
- backfillInvoices = async (invoiceIds) => {
2771
+ backfillInvoices = async (invoiceIds, accountId) => {
2026
2772
  const missingIds = await this.postgresClient.findMissingEntries("invoices", invoiceIds);
2027
2773
  await this.fetchMissingEntities(missingIds, (id) => this.stripe.invoices.retrieve(id)).then(
2028
- (entries) => this.upsertInvoices(entries)
2774
+ (entries) => this.upsertInvoices(entries, accountId)
2029
2775
  );
2030
2776
  };
2031
- backfillPrices = async (priceIds) => {
2777
+ backfillPrices = async (priceIds, accountId) => {
2032
2778
  const missingIds = await this.postgresClient.findMissingEntries("prices", priceIds);
2033
2779
  await this.fetchMissingEntities(missingIds, (id) => this.stripe.prices.retrieve(id)).then(
2034
- (entries) => this.upsertPrices(entries)
2780
+ (entries) => this.upsertPrices(entries, accountId)
2035
2781
  );
2036
2782
  };
2037
- async upsertPlans(plans, backfillRelatedEntities, syncTimestamp) {
2783
+ async upsertPlans(plans, accountId, backfillRelatedEntities, syncTimestamp) {
2038
2784
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2039
- await this.backfillProducts(getUniqueIds(plans, "product"));
2785
+ await this.backfillProducts(getUniqueIds(plans, "product"), accountId);
2040
2786
  }
2041
2787
  return this.postgresClient.upsertManyWithTimestampProtection(
2042
2788
  plans,
2043
2789
  "plans",
2044
- planSchema,
2790
+ accountId,
2045
2791
  syncTimestamp
2046
2792
  );
2047
2793
  }
2048
2794
  async deletePlan(id) {
2049
2795
  return this.postgresClient.delete("plans", id);
2050
2796
  }
2051
- async upsertPrices(prices, backfillRelatedEntities, syncTimestamp) {
2797
+ async upsertPrices(prices, accountId, backfillRelatedEntities, syncTimestamp) {
2052
2798
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2053
- await this.backfillProducts(getUniqueIds(prices, "product"));
2799
+ await this.backfillProducts(getUniqueIds(prices, "product"), accountId);
2054
2800
  }
2055
2801
  return this.postgresClient.upsertManyWithTimestampProtection(
2056
2802
  prices,
2057
2803
  "prices",
2058
- priceSchema,
2804
+ accountId,
2059
2805
  syncTimestamp
2060
2806
  );
2061
2807
  }
2062
2808
  async deletePrice(id) {
2063
2809
  return this.postgresClient.delete("prices", id);
2064
2810
  }
2065
- async upsertProducts(products, syncTimestamp) {
2811
+ async upsertProducts(products, accountId, syncTimestamp) {
2066
2812
  return this.postgresClient.upsertManyWithTimestampProtection(
2067
2813
  products,
2068
2814
  "products",
2069
- productSchema,
2815
+ accountId,
2070
2816
  syncTimestamp
2071
2817
  );
2072
2818
  }
2073
2819
  async deleteProduct(id) {
2074
2820
  return this.postgresClient.delete("products", id);
2075
2821
  }
2076
- async backfillProducts(productIds) {
2822
+ async backfillProducts(productIds, accountId) {
2077
2823
  const missingProductIds = await this.postgresClient.findMissingEntries("products", productIds);
2078
2824
  await this.fetchMissingEntities(
2079
2825
  missingProductIds,
2080
2826
  (id) => this.stripe.products.retrieve(id)
2081
- ).then((products) => this.upsertProducts(products));
2827
+ ).then((products) => this.upsertProducts(products, accountId));
2082
2828
  }
2083
- async upsertPaymentIntents(paymentIntents, backfillRelatedEntities, syncTimestamp) {
2829
+ async upsertPaymentIntents(paymentIntents, accountId, backfillRelatedEntities, syncTimestamp) {
2084
2830
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2085
2831
  await Promise.all([
2086
- this.backfillCustomers(getUniqueIds(paymentIntents, "customer")),
2087
- this.backfillInvoices(getUniqueIds(paymentIntents, "invoice"))
2832
+ this.backfillCustomers(getUniqueIds(paymentIntents, "customer"), accountId),
2833
+ this.backfillInvoices(getUniqueIds(paymentIntents, "invoice"), accountId)
2088
2834
  ]);
2089
2835
  }
2090
2836
  return this.postgresClient.upsertManyWithTimestampProtection(
2091
2837
  paymentIntents,
2092
2838
  "payment_intents",
2093
- paymentIntentSchema,
2839
+ accountId,
2094
2840
  syncTimestamp
2095
2841
  );
2096
2842
  }
2097
- async upsertPaymentMethods(paymentMethods, backfillRelatedEntities = false, syncTimestamp) {
2843
+ async upsertPaymentMethods(paymentMethods, accountId, backfillRelatedEntities = false, syncTimestamp) {
2098
2844
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2099
- await this.backfillCustomers(getUniqueIds(paymentMethods, "customer"));
2845
+ await this.backfillCustomers(getUniqueIds(paymentMethods, "customer"), accountId);
2100
2846
  }
2101
2847
  return this.postgresClient.upsertManyWithTimestampProtection(
2102
2848
  paymentMethods,
2103
2849
  "payment_methods",
2104
- paymentMethodsSchema,
2850
+ accountId,
2105
2851
  syncTimestamp
2106
2852
  );
2107
2853
  }
2108
- async upsertSetupIntents(setupIntents, backfillRelatedEntities, syncTimestamp) {
2854
+ async upsertSetupIntents(setupIntents, accountId, backfillRelatedEntities, syncTimestamp) {
2109
2855
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2110
- await this.backfillCustomers(getUniqueIds(setupIntents, "customer"));
2856
+ await this.backfillCustomers(getUniqueIds(setupIntents, "customer"), accountId);
2111
2857
  }
2112
2858
  return this.postgresClient.upsertManyWithTimestampProtection(
2113
2859
  setupIntents,
2114
2860
  "setup_intents",
2115
- setupIntentsSchema,
2861
+ accountId,
2116
2862
  syncTimestamp
2117
2863
  );
2118
2864
  }
2119
- async upsertTaxIds(taxIds, backfillRelatedEntities, syncTimestamp) {
2865
+ async upsertTaxIds(taxIds, accountId, backfillRelatedEntities, syncTimestamp) {
2120
2866
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2121
- await this.backfillCustomers(getUniqueIds(taxIds, "customer"));
2867
+ await this.backfillCustomers(getUniqueIds(taxIds, "customer"), accountId);
2122
2868
  }
2123
2869
  return this.postgresClient.upsertManyWithTimestampProtection(
2124
2870
  taxIds,
2125
2871
  "tax_ids",
2126
- taxIdSchema,
2872
+ accountId,
2127
2873
  syncTimestamp
2128
2874
  );
2129
2875
  }
2130
2876
  async deleteTaxId(id) {
2131
2877
  return this.postgresClient.delete("tax_ids", id);
2132
2878
  }
2133
- async upsertSubscriptionItems(subscriptionItems, syncTimestamp) {
2879
+ async upsertSubscriptionItems(subscriptionItems, accountId, syncTimestamp) {
2134
2880
  const modifiedSubscriptionItems = subscriptionItems.map((subscriptionItem) => {
2135
2881
  const priceId = subscriptionItem.price.id.toString();
2136
2882
  const deleted = subscriptionItem.deleted;
@@ -2145,11 +2891,11 @@ var StripeSync = class {
2145
2891
  await this.postgresClient.upsertManyWithTimestampProtection(
2146
2892
  modifiedSubscriptionItems,
2147
2893
  "subscription_items",
2148
- subscriptionItemSchema,
2894
+ accountId,
2149
2895
  syncTimestamp
2150
2896
  );
2151
2897
  }
2152
- async fillCheckoutSessionsLineItems(checkoutSessionIds, syncTimestamp) {
2898
+ async fillCheckoutSessionsLineItems(checkoutSessionIds, accountId, syncTimestamp) {
2153
2899
  for (const checkoutSessionId of checkoutSessionIds) {
2154
2900
  const lineItemResponses = [];
2155
2901
  for await (const lineItem of this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
@@ -2157,12 +2903,18 @@ var StripeSync = class {
2157
2903
  })) {
2158
2904
  lineItemResponses.push(lineItem);
2159
2905
  }
2160
- await this.upsertCheckoutSessionLineItems(lineItemResponses, checkoutSessionId, syncTimestamp);
2906
+ await this.upsertCheckoutSessionLineItems(
2907
+ lineItemResponses,
2908
+ checkoutSessionId,
2909
+ accountId,
2910
+ syncTimestamp
2911
+ );
2161
2912
  }
2162
2913
  }
2163
- async upsertCheckoutSessionLineItems(lineItems, checkoutSessionId, syncTimestamp) {
2914
+ async upsertCheckoutSessionLineItems(lineItems, checkoutSessionId, accountId, syncTimestamp) {
2164
2915
  await this.backfillPrices(
2165
- lineItems.map((lineItem) => lineItem.price?.id?.toString() ?? void 0).filter((id) => id !== void 0)
2916
+ lineItems.map((lineItem) => lineItem.price?.id?.toString() ?? void 0).filter((id) => id !== void 0),
2917
+ accountId
2166
2918
  );
2167
2919
  const modifiedLineItems = lineItems.map((lineItem) => {
2168
2920
  const priceId = typeof lineItem.price === "object" && lineItem.price?.id ? lineItem.price.id.toString() : lineItem.price?.toString() || null;
@@ -2175,14 +2927,14 @@ var StripeSync = class {
2175
2927
  await this.postgresClient.upsertManyWithTimestampProtection(
2176
2928
  modifiedLineItems,
2177
2929
  "checkout_session_line_items",
2178
- checkoutSessionLineItemSchema,
2930
+ accountId,
2179
2931
  syncTimestamp
2180
2932
  );
2181
2933
  }
2182
2934
  async markDeletedSubscriptionItems(subscriptionId, currentSubItemIds) {
2183
2935
  let prepared = (0, import_yesql2.pg)(`
2184
- select id from "${this.config.schema}"."subscription_items"
2185
- where subscription = :subscriptionId and deleted = false;
2936
+ select id from "stripe"."subscription_items"
2937
+ where subscription = :subscriptionId and COALESCE(deleted, false) = false;
2186
2938
  `)({ subscriptionId });
2187
2939
  const { rows } = await this.postgresClient.query(prepared.text, prepared.values);
2188
2940
  const deletedIds = rows.filter(
@@ -2191,32 +2943,33 @@ var StripeSync = class {
2191
2943
  if (deletedIds.length > 0) {
2192
2944
  const ids = deletedIds.map(({ id }) => id);
2193
2945
  prepared = (0, import_yesql2.pg)(`
2194
- update "${this.config.schema}"."subscription_items"
2195
- set deleted = true where id=any(:ids::text[]);
2946
+ update "stripe"."subscription_items"
2947
+ set _raw_data = jsonb_set(_raw_data, '{deleted}', 'true'::jsonb)
2948
+ where id=any(:ids::text[]);
2196
2949
  `)({ ids });
2197
- const { rowCount } = await await this.postgresClient.query(prepared.text, prepared.values);
2950
+ const { rowCount } = await this.postgresClient.query(prepared.text, prepared.values);
2198
2951
  return { rowCount: rowCount || 0 };
2199
2952
  } else {
2200
2953
  return { rowCount: 0 };
2201
2954
  }
2202
2955
  }
2203
- async upsertSubscriptionSchedules(subscriptionSchedules, backfillRelatedEntities, syncTimestamp) {
2956
+ async upsertSubscriptionSchedules(subscriptionSchedules, accountId, backfillRelatedEntities, syncTimestamp) {
2204
2957
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2205
2958
  const customerIds = getUniqueIds(subscriptionSchedules, "customer");
2206
- await this.backfillCustomers(customerIds);
2959
+ await this.backfillCustomers(customerIds, accountId);
2207
2960
  }
2208
2961
  const rows = await this.postgresClient.upsertManyWithTimestampProtection(
2209
2962
  subscriptionSchedules,
2210
2963
  "subscription_schedules",
2211
- subscriptionScheduleSchema,
2964
+ accountId,
2212
2965
  syncTimestamp
2213
2966
  );
2214
2967
  return rows;
2215
2968
  }
2216
- async upsertSubscriptions(subscriptions, backfillRelatedEntities, syncTimestamp) {
2969
+ async upsertSubscriptions(subscriptions, accountId, backfillRelatedEntities, syncTimestamp) {
2217
2970
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2218
2971
  const customerIds = getUniqueIds(subscriptions, "customer");
2219
- await this.backfillCustomers(customerIds);
2972
+ await this.backfillCustomers(customerIds, accountId);
2220
2973
  }
2221
2974
  await this.expandEntity(
2222
2975
  subscriptions,
@@ -2226,11 +2979,11 @@ var StripeSync = class {
2226
2979
  const rows = await this.postgresClient.upsertManyWithTimestampProtection(
2227
2980
  subscriptions,
2228
2981
  "subscriptions",
2229
- subscriptionSchema,
2982
+ accountId,
2230
2983
  syncTimestamp
2231
2984
  );
2232
2985
  const allSubscriptionItems = subscriptions.flatMap((subscription) => subscription.items.data);
2233
- await this.upsertSubscriptionItems(allSubscriptionItems, syncTimestamp);
2986
+ await this.upsertSubscriptionItems(allSubscriptionItems, accountId, syncTimestamp);
2234
2987
  const markSubscriptionItemsDeleted = [];
2235
2988
  for (const subscription of subscriptions) {
2236
2989
  const subscriptionItems = subscription.items.data;
@@ -2244,35 +2997,35 @@ var StripeSync = class {
2244
2997
  }
2245
2998
  async deleteRemovedActiveEntitlements(customerId, currentActiveEntitlementIds) {
2246
2999
  const prepared = (0, import_yesql2.pg)(`
2247
- delete from "${this.config.schema}"."active_entitlements"
3000
+ delete from "stripe"."active_entitlements"
2248
3001
  where customer = :customerId and id <> ALL(:currentActiveEntitlementIds::text[]);
2249
3002
  `)({ customerId, currentActiveEntitlementIds });
2250
3003
  const { rowCount } = await this.postgresClient.query(prepared.text, prepared.values);
2251
3004
  return { rowCount: rowCount || 0 };
2252
3005
  }
2253
- async upsertFeatures(features, syncTimestamp) {
3006
+ async upsertFeatures(features, accountId, syncTimestamp) {
2254
3007
  return this.postgresClient.upsertManyWithTimestampProtection(
2255
3008
  features,
2256
3009
  "features",
2257
- featureSchema,
3010
+ accountId,
2258
3011
  syncTimestamp
2259
3012
  );
2260
3013
  }
2261
- async backfillFeatures(featureIds) {
3014
+ async backfillFeatures(featureIds, accountId) {
2262
3015
  const missingFeatureIds = await this.postgresClient.findMissingEntries("features", featureIds);
2263
3016
  await this.fetchMissingEntities(
2264
3017
  missingFeatureIds,
2265
3018
  (id) => this.stripe.entitlements.features.retrieve(id)
2266
- ).then((features) => this.upsertFeatures(features)).catch((err) => {
3019
+ ).then((features) => this.upsertFeatures(features, accountId)).catch((err) => {
2267
3020
  this.config.logger?.error(err, "Failed to backfill features");
2268
3021
  throw err;
2269
3022
  });
2270
3023
  }
2271
- async upsertActiveEntitlements(customerId, activeEntitlements, backfillRelatedEntities, syncTimestamp) {
3024
+ async upsertActiveEntitlements(customerId, activeEntitlements, accountId, backfillRelatedEntities, syncTimestamp) {
2272
3025
  if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
2273
3026
  await Promise.all([
2274
- this.backfillCustomers(getUniqueIds(activeEntitlements, "customer")),
2275
- this.backfillFeatures(getUniqueIds(activeEntitlements, "feature"))
3027
+ this.backfillCustomers(getUniqueIds(activeEntitlements, "customer"), accountId),
3028
+ this.backfillFeatures(getUniqueIds(activeEntitlements, "feature"), accountId)
2276
3029
  ]);
2277
3030
  }
2278
3031
  const entitlements = activeEntitlements.map((entitlement) => ({
@@ -2286,77 +3039,159 @@ var StripeSync = class {
2286
3039
  return this.postgresClient.upsertManyWithTimestampProtection(
2287
3040
  entitlements,
2288
3041
  "active_entitlements",
2289
- activeEntitlementSchema,
3042
+ accountId,
2290
3043
  syncTimestamp
2291
3044
  );
2292
3045
  }
2293
- // Managed Webhook CRUD methods
2294
- async createManagedWebhook(baseUrl, params) {
2295
- const uuid = (0, import_node_crypto.randomUUID)();
2296
- const webhookUrl = `${baseUrl}/${uuid}`;
2297
- const webhook = await this.stripe.webhookEndpoints.create({
2298
- ...params,
2299
- url: webhookUrl
2300
- });
2301
- const webhookWithUuid = { ...webhook, uuid };
2302
- await this.upsertManagedWebhooks([webhookWithUuid]);
2303
- return { webhook, uuid };
2304
- }
2305
- async findOrCreateManagedWebhook(baseUrl, params) {
2306
- const existingWebhooks = await this.listManagedWebhooks();
2307
- for (const existingWebhook of existingWebhooks) {
2308
- const existingBaseUrl = existingWebhook.url.replace(/\/[^/]+$/, "");
2309
- if (existingBaseUrl === baseUrl) {
3046
+ async findOrCreateManagedWebhook(url, params) {
3047
+ const webhookParams = {
3048
+ enabled_events: this.getSupportedEventTypes(),
3049
+ ...params
3050
+ };
3051
+ const accountId = await this.getAccountId();
3052
+ const lockKey = `webhook:${accountId}:${url}`;
3053
+ return this.postgresClient.withAdvisoryLock(lockKey, async () => {
3054
+ const existingWebhook = await this.getManagedWebhookByUrl(url);
3055
+ if (existingWebhook) {
2310
3056
  try {
2311
- const stripeWebhook = await this.stripe.webhookEndpoints.retrieve(
2312
- existingWebhook.id
2313
- );
3057
+ const stripeWebhook = await this.stripe.webhookEndpoints.retrieve(existingWebhook.id);
2314
3058
  if (stripeWebhook.status === "enabled") {
2315
- return {
2316
- webhook: stripeWebhook,
2317
- uuid: existingWebhook.uuid
2318
- };
3059
+ return stripeWebhook;
2319
3060
  }
3061
+ this.config.logger?.info(
3062
+ { webhookId: existingWebhook.id },
3063
+ "Webhook is disabled, deleting and will recreate"
3064
+ );
3065
+ await this.stripe.webhookEndpoints.del(existingWebhook.id);
3066
+ await this.postgresClient.delete("_managed_webhooks", existingWebhook.id);
2320
3067
  } catch (error) {
2321
- continue;
3068
+ const stripeError = error;
3069
+ if (stripeError?.statusCode === 404 || stripeError?.code === "resource_missing") {
3070
+ this.config.logger?.warn(
3071
+ { error, webhookId: existingWebhook.id },
3072
+ "Webhook not found in Stripe (404), removing from database"
3073
+ );
3074
+ await this.postgresClient.delete("_managed_webhooks", existingWebhook.id);
3075
+ } else {
3076
+ this.config.logger?.error(
3077
+ { error, webhookId: existingWebhook.id },
3078
+ "Error retrieving webhook from Stripe, keeping in database"
3079
+ );
3080
+ throw error;
3081
+ }
2322
3082
  }
2323
3083
  }
2324
- }
2325
- return this.createManagedWebhook(baseUrl, params);
3084
+ const allDbWebhooks = await this.listManagedWebhooks();
3085
+ for (const dbWebhook of allDbWebhooks) {
3086
+ if (dbWebhook.url !== url) {
3087
+ this.config.logger?.info(
3088
+ { webhookId: dbWebhook.id, oldUrl: dbWebhook.url, newUrl: url },
3089
+ "Webhook URL mismatch, deleting"
3090
+ );
3091
+ try {
3092
+ await this.stripe.webhookEndpoints.del(dbWebhook.id);
3093
+ } catch (error) {
3094
+ this.config.logger?.warn(
3095
+ { error, webhookId: dbWebhook.id },
3096
+ "Failed to delete old webhook from Stripe"
3097
+ );
3098
+ }
3099
+ await this.postgresClient.delete("_managed_webhooks", dbWebhook.id);
3100
+ }
3101
+ }
3102
+ try {
3103
+ const stripeWebhooks = await this.stripe.webhookEndpoints.list({ limit: 100 });
3104
+ for (const stripeWebhook of stripeWebhooks.data) {
3105
+ const isManagedByMetadata = stripeWebhook.metadata?.managed_by?.toLowerCase().replace(/[\s\-]+/g, "") === "stripesync";
3106
+ const normalizedDescription = stripeWebhook.description?.toLowerCase().replace(/[\s\-]+/g, "") || "";
3107
+ const isManagedByDescription = normalizedDescription.includes("stripesync");
3108
+ if (isManagedByMetadata || isManagedByDescription) {
3109
+ const existsInDb = allDbWebhooks.some((dbWebhook) => dbWebhook.id === stripeWebhook.id);
3110
+ if (!existsInDb) {
3111
+ this.config.logger?.warn(
3112
+ { webhookId: stripeWebhook.id, url: stripeWebhook.url },
3113
+ "Found orphaned managed webhook in Stripe, deleting"
3114
+ );
3115
+ await this.stripe.webhookEndpoints.del(stripeWebhook.id);
3116
+ }
3117
+ }
3118
+ }
3119
+ } catch (error) {
3120
+ this.config.logger?.warn({ error }, "Failed to check for orphaned webhooks");
3121
+ }
3122
+ const webhook = await this.stripe.webhookEndpoints.create({
3123
+ ...webhookParams,
3124
+ url,
3125
+ // Always set metadata to identify managed webhooks
3126
+ metadata: {
3127
+ ...webhookParams.metadata,
3128
+ managed_by: "stripe-sync",
3129
+ version: package_default.version
3130
+ }
3131
+ });
3132
+ const accountId2 = await this.getAccountId();
3133
+ await this.upsertManagedWebhooks([webhook], accountId2);
3134
+ return webhook;
3135
+ });
2326
3136
  }
2327
3137
  async getManagedWebhook(id) {
3138
+ const accountId = await this.getAccountId();
2328
3139
  const result = await this.postgresClient.query(
2329
- `SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."_managed_webhooks" WHERE id = $1`,
2330
- [id]
3140
+ `SELECT * FROM "stripe"."_managed_webhooks" WHERE id = $1 AND "account_id" = $2`,
3141
+ [id, accountId]
3142
+ );
3143
+ return result.rows.length > 0 ? result.rows[0] : null;
3144
+ }
3145
+ /**
3146
+ * Get a managed webhook by URL and account ID.
3147
+ * Used for race condition recovery: when createManagedWebhook hits a unique constraint
3148
+ * violation (another instance created the webhook), we need to fetch the existing webhook
3149
+ * by URL since we only know the URL, not the ID of the webhook that won the race.
3150
+ */
3151
+ async getManagedWebhookByUrl(url) {
3152
+ const accountId = await this.getAccountId();
3153
+ const result = await this.postgresClient.query(
3154
+ `SELECT * FROM "stripe"."_managed_webhooks" WHERE url = $1 AND "account_id" = $2`,
3155
+ [url, accountId]
2331
3156
  );
2332
3157
  return result.rows.length > 0 ? result.rows[0] : null;
2333
3158
  }
2334
3159
  async listManagedWebhooks() {
3160
+ const accountId = await this.getAccountId();
2335
3161
  const result = await this.postgresClient.query(
2336
- `SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."_managed_webhooks" ORDER BY created DESC`
3162
+ `SELECT * FROM "stripe"."_managed_webhooks" WHERE "account_id" = $1 ORDER BY created DESC`,
3163
+ [accountId]
2337
3164
  );
2338
3165
  return result.rows;
2339
3166
  }
2340
3167
  async updateManagedWebhook(id, params) {
2341
3168
  const webhook = await this.stripe.webhookEndpoints.update(id, params);
2342
- const existing = await this.getManagedWebhook(id);
2343
- const webhookWithUuid = { ...webhook, uuid: existing?.uuid || (0, import_node_crypto.randomUUID)() };
2344
- await this.upsertManagedWebhooks([webhookWithUuid]);
3169
+ const accountId = await this.getAccountId();
3170
+ await this.upsertManagedWebhooks([webhook], accountId);
2345
3171
  return webhook;
2346
3172
  }
2347
3173
  async deleteManagedWebhook(id) {
2348
3174
  await this.stripe.webhookEndpoints.del(id);
2349
3175
  return this.postgresClient.delete("_managed_webhooks", id);
2350
3176
  }
2351
- async upsertManagedWebhooks(webhooks, syncTimestamp) {
3177
+ async upsertManagedWebhooks(webhooks, accountId, syncTimestamp) {
3178
+ const filteredWebhooks = webhooks.map((webhook) => {
3179
+ const filtered = {};
3180
+ for (const prop of managedWebhookSchema.properties) {
3181
+ if (prop in webhook) {
3182
+ filtered[prop] = webhook[prop];
3183
+ }
3184
+ }
3185
+ return filtered;
3186
+ });
2352
3187
  return this.postgresClient.upsertManyWithTimestampProtection(
2353
- webhooks,
3188
+ filteredWebhooks,
2354
3189
  "_managed_webhooks",
2355
- managedWebhookSchema,
3190
+ accountId,
2356
3191
  syncTimestamp
2357
3192
  );
2358
3193
  }
2359
- async backfillSubscriptions(subscriptionIds) {
3194
+ async backfillSubscriptions(subscriptionIds, accountId) {
2360
3195
  const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
2361
3196
  "subscriptions",
2362
3197
  subscriptionIds
@@ -2364,9 +3199,9 @@ var StripeSync = class {
2364
3199
  await this.fetchMissingEntities(
2365
3200
  missingSubscriptionIds,
2366
3201
  (id) => this.stripe.subscriptions.retrieve(id)
2367
- ).then((subscriptions) => this.upsertSubscriptions(subscriptions));
3202
+ ).then((subscriptions) => this.upsertSubscriptions(subscriptions, accountId));
2368
3203
  }
2369
- backfillSubscriptionSchedules = async (subscriptionIds) => {
3204
+ backfillSubscriptionSchedules = async (subscriptionIds, accountId) => {
2370
3205
  const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
2371
3206
  "subscription_schedules",
2372
3207
  subscriptionIds
@@ -2374,7 +3209,9 @@ var StripeSync = class {
2374
3209
  await this.fetchMissingEntities(
2375
3210
  missingSubscriptionIds,
2376
3211
  (id) => this.stripe.subscriptionSchedules.retrieve(id)
2377
- ).then((subscriptionSchedules) => this.upsertSubscriptionSchedules(subscriptionSchedules));
3212
+ ).then(
3213
+ (subscriptionSchedules) => this.upsertSubscriptionSchedules(subscriptionSchedules, accountId)
3214
+ );
2378
3215
  };
2379
3216
  /**
2380
3217
  * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
@@ -2412,9 +3249,93 @@ function chunkArray(array, chunkSize) {
2412
3249
  }
2413
3250
  return result;
2414
3251
  }
3252
+
3253
+ // src/database/migrate.ts
3254
+ var import_pg_node_migrations = require("pg-node-migrations");
3255
+ var import_node_fs = __toESM(require("fs"), 1);
3256
+ var import_node_path = __toESM(require("path"), 1);
3257
+ var import_node_url = require("url");
3258
+ var __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
3259
+ var __dirname = import_node_path.default.dirname(__filename2);
3260
+ async function doesTableExist(client, schema, tableName) {
3261
+ const result = await client.query({
3262
+ text: `SELECT EXISTS (
3263
+ SELECT 1
3264
+ FROM information_schema.tables
3265
+ WHERE table_schema = $1
3266
+ AND table_name = $2
3267
+ )`,
3268
+ values: [schema, tableName]
3269
+ });
3270
+ return result.rows[0]?.exists || false;
3271
+ }
3272
+ async function renameMigrationsTableIfNeeded(client, schema = "stripe", logger) {
3273
+ const oldTableExists = await doesTableExist(client, schema, "migrations");
3274
+ const newTableExists = await doesTableExist(client, schema, "_migrations");
3275
+ if (oldTableExists && !newTableExists) {
3276
+ logger?.info("Renaming migrations table to _migrations");
3277
+ await client.query(`ALTER TABLE "${schema}"."migrations" RENAME TO "_migrations"`);
3278
+ logger?.info("Successfully renamed migrations table");
3279
+ }
3280
+ }
3281
+ async function cleanupSchema(client, schema, logger) {
3282
+ logger?.warn(`Migrations table is empty - dropping and recreating schema "${schema}"`);
3283
+ await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
3284
+ await client.query(`CREATE SCHEMA "${schema}"`);
3285
+ logger?.info(`Schema "${schema}" has been reset`);
3286
+ }
3287
+ async function connectAndMigrate(client, migrationsDirectory, logger, logOnError = false) {
3288
+ if (!import_node_fs.default.existsSync(migrationsDirectory)) {
3289
+ logger?.info(`Migrations directory ${migrationsDirectory} not found, skipping`);
3290
+ return;
3291
+ }
3292
+ const optionalConfig = {
3293
+ schemaName: "stripe",
3294
+ tableName: "_migrations"
3295
+ };
3296
+ try {
3297
+ await (0, import_pg_node_migrations.migrate)({ client }, migrationsDirectory, optionalConfig);
3298
+ } catch (error) {
3299
+ if (logOnError && error instanceof Error) {
3300
+ logger?.error(error, "Migration error:");
3301
+ } else {
3302
+ throw error;
3303
+ }
3304
+ }
3305
+ }
3306
+ async function runMigrations(adapter, logger) {
3307
+ const client = adapter.toPgClient();
3308
+ const schema = "stripe";
3309
+ try {
3310
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${schema};`);
3311
+ await renameMigrationsTableIfNeeded(client, schema, logger);
3312
+ const tableExists = await doesTableExist(client, schema, "_migrations");
3313
+ if (tableExists) {
3314
+ const migrationCount = await client.query(
3315
+ `SELECT COUNT(*) as count FROM "${schema}"."_migrations"`
3316
+ );
3317
+ const isEmpty = migrationCount.rows[0]?.count === "0";
3318
+ if (isEmpty) {
3319
+ await cleanupSchema(client, schema, logger);
3320
+ }
3321
+ }
3322
+ logger?.info("Running migrations");
3323
+ await connectAndMigrate(client, import_node_path.default.resolve(__dirname, "./migrations"), logger);
3324
+ } catch (err) {
3325
+ logger?.error(err, "Error running migrations");
3326
+ throw err;
3327
+ } finally {
3328
+ logger?.info("Finished migrations");
3329
+ }
3330
+ }
3331
+
3332
+ // src/index.ts
3333
+ var VERSION = package_default.version;
2415
3334
  // Annotate the CommonJS export names for ESM import in node:
2416
3335
  0 && (module.exports = {
2417
3336
  PostgresClient,
2418
- StripeAutoSync,
3337
+ StripeSync,
3338
+ VERSION,
3339
+ hashApiKey,
2419
3340
  runMigrations
2420
3341
  });