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