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