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