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