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