stripe-experiment-sync 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/index.cjs +2370 -0
- package/dist/index.d.cts +222 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +2328 -0
- package/dist/migrations/0000_initial_migration.sql +1 -0
- package/dist/migrations/0001_products.sql +17 -0
- package/dist/migrations/0002_customers.sql +23 -0
- package/dist/migrations/0003_prices.sql +34 -0
- package/dist/migrations/0004_subscriptions.sql +56 -0
- package/dist/migrations/0005_invoices.sql +77 -0
- package/dist/migrations/0006_charges.sql +43 -0
- package/dist/migrations/0007_coupons.sql +19 -0
- package/dist/migrations/0008_disputes.sql +17 -0
- package/dist/migrations/0009_events.sql +12 -0
- package/dist/migrations/0010_payouts.sql +30 -0
- package/dist/migrations/0011_plans.sql +25 -0
- package/dist/migrations/0012_add_updated_at.sql +108 -0
- package/dist/migrations/0013_add_subscription_items.sql +12 -0
- package/dist/migrations/0014_migrate_subscription_items.sql +26 -0
- package/dist/migrations/0015_add_customer_deleted.sql +2 -0
- package/dist/migrations/0016_add_invoice_indexes.sql +2 -0
- package/dist/migrations/0017_drop_charges_unavailable_columns.sql +6 -0
- package/dist/migrations/0018_setup_intents.sql +17 -0
- package/dist/migrations/0019_payment_methods.sql +12 -0
- package/dist/migrations/0020_disputes_payment_intent_created_idx.sql +3 -0
- package/dist/migrations/0021_payment_intent.sql +42 -0
- package/dist/migrations/0022_adjust_plans.sql +5 -0
- package/dist/migrations/0023_invoice_deleted.sql +1 -0
- package/dist/migrations/0024_subscription_schedules.sql +29 -0
- package/dist/migrations/0025_tax_ids.sql +14 -0
- package/dist/migrations/0026_credit_notes.sql +36 -0
- package/dist/migrations/0027_add_marketing_features_to_products.sql +2 -0
- package/dist/migrations/0028_early_fraud_warning.sql +22 -0
- package/dist/migrations/0029_reviews.sql +28 -0
- package/dist/migrations/0030_refunds.sql +29 -0
- package/dist/migrations/0031_add_default_price.sql +2 -0
- package/dist/migrations/0032_update_subscription_items.sql +3 -0
- package/dist/migrations/0033_add_last_synced_at.sql +85 -0
- package/dist/migrations/0034_remove_foreign_keys.sql +13 -0
- package/dist/migrations/0035_checkout_sessions.sql +77 -0
- package/dist/migrations/0036_checkout_session_line_items.sql +24 -0
- package/dist/migrations/0037_add_features.sql +18 -0
- package/dist/migrations/0038_active_entitlement.sql +20 -0
- package/dist/migrations/0039_add_paused_to_subscription_status.sql +1 -0
- package/dist/migrations/0040_managed_webhooks.sql +28 -0
- package/package.json +60 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
PostgresClient: () => PostgresClient,
|
|
34
|
+
StripeAutoSync: () => StripeAutoSync
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// ../../node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3_yaml@2.8.1/node_modules/tsup/assets/cjs_shims.js
|
|
39
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
|
|
40
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
41
|
+
|
|
42
|
+
// src/stripeSync.ts
|
|
43
|
+
var import_stripe = __toESM(require("stripe"), 1);
|
|
44
|
+
var import_yesql2 = require("yesql");
|
|
45
|
+
|
|
46
|
+
// src/database/postgres.ts
|
|
47
|
+
var import_pg = __toESM(require("pg"), 1);
|
|
48
|
+
var import_yesql = require("yesql");
|
|
49
|
+
var PostgresClient = class {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.pool = new import_pg.default.Pool(config.poolConfig);
|
|
53
|
+
}
|
|
54
|
+
pool;
|
|
55
|
+
async delete(table, id) {
|
|
56
|
+
const prepared = (0, import_yesql.pg)(`
|
|
57
|
+
delete from "${this.config.schema}"."${table}"
|
|
58
|
+
where id = :id
|
|
59
|
+
returning id;
|
|
60
|
+
`)({ id });
|
|
61
|
+
const { rows } = await this.query(prepared.text, prepared.values);
|
|
62
|
+
return rows.length > 0;
|
|
63
|
+
}
|
|
64
|
+
async query(text, params) {
|
|
65
|
+
return this.pool.query(text, params);
|
|
66
|
+
}
|
|
67
|
+
async upsertMany(entries, table, tableSchema) {
|
|
68
|
+
if (!entries.length) return [];
|
|
69
|
+
const chunkSize = 5;
|
|
70
|
+
const results = [];
|
|
71
|
+
for (let i = 0; i < entries.length; i += chunkSize) {
|
|
72
|
+
const chunk = entries.slice(i, i + chunkSize);
|
|
73
|
+
const queries = [];
|
|
74
|
+
chunk.forEach((entry) => {
|
|
75
|
+
const cleansed = this.cleanseArrayField(entry);
|
|
76
|
+
const upsertSql = this.constructUpsertSql(this.config.schema, table, tableSchema);
|
|
77
|
+
const prepared = (0, import_yesql.pg)(upsertSql, {
|
|
78
|
+
useNullForMissing: true
|
|
79
|
+
})(cleansed);
|
|
80
|
+
queries.push(this.pool.query(prepared.text, prepared.values));
|
|
81
|
+
});
|
|
82
|
+
results.push(...await Promise.all(queries));
|
|
83
|
+
}
|
|
84
|
+
return results.flatMap((it) => it.rows);
|
|
85
|
+
}
|
|
86
|
+
async upsertManyWithTimestampProtection(entries, table, tableSchema, syncTimestamp) {
|
|
87
|
+
const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
88
|
+
if (!entries.length) return [];
|
|
89
|
+
const chunkSize = 5;
|
|
90
|
+
const results = [];
|
|
91
|
+
for (let i = 0; i < entries.length; i += chunkSize) {
|
|
92
|
+
const chunk = entries.slice(i, i + chunkSize);
|
|
93
|
+
const queries = [];
|
|
94
|
+
chunk.forEach((entry) => {
|
|
95
|
+
const cleansed = this.cleanseArrayField(entry);
|
|
96
|
+
cleansed.last_synced_at = timestamp;
|
|
97
|
+
const upsertSql = this.constructUpsertWithTimestampProtectionSql(
|
|
98
|
+
this.config.schema,
|
|
99
|
+
table,
|
|
100
|
+
tableSchema
|
|
101
|
+
);
|
|
102
|
+
const prepared = (0, import_yesql.pg)(upsertSql, {
|
|
103
|
+
useNullForMissing: true
|
|
104
|
+
})(cleansed);
|
|
105
|
+
queries.push(this.pool.query(prepared.text, prepared.values));
|
|
106
|
+
});
|
|
107
|
+
results.push(...await Promise.all(queries));
|
|
108
|
+
}
|
|
109
|
+
return results.flatMap((it) => it.rows);
|
|
110
|
+
}
|
|
111
|
+
async findMissingEntries(table, ids) {
|
|
112
|
+
if (!ids.length) return [];
|
|
113
|
+
const prepared = (0, import_yesql.pg)(`
|
|
114
|
+
select id from "${this.config.schema}"."${table}"
|
|
115
|
+
where id=any(:ids::text[]);
|
|
116
|
+
`)({ ids });
|
|
117
|
+
const { rows } = await this.query(prepared.text, prepared.values);
|
|
118
|
+
const existingIds = rows.map((it) => it.id);
|
|
119
|
+
const missingIds = ids.filter((it) => !existingIds.includes(it));
|
|
120
|
+
return missingIds;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns an (yesql formatted) upsert function based on the key/vals of an object.
|
|
124
|
+
* eg,
|
|
125
|
+
* insert into customers ("id", "name")
|
|
126
|
+
* values (:id, :name)
|
|
127
|
+
* on conflict (id)
|
|
128
|
+
* do update set (
|
|
129
|
+
* "id" = :id,
|
|
130
|
+
* "name" = :name
|
|
131
|
+
* )
|
|
132
|
+
*/
|
|
133
|
+
constructUpsertSql(schema, table, tableSchema, options) {
|
|
134
|
+
const { conflict = "id" } = options || {};
|
|
135
|
+
const properties = tableSchema.properties;
|
|
136
|
+
return `
|
|
137
|
+
insert into "${schema}"."${table}" (
|
|
138
|
+
${properties.map((x) => `"${x}"`).join(",")}
|
|
139
|
+
)
|
|
140
|
+
values (
|
|
141
|
+
${properties.map((x) => `:${x}`).join(",")}
|
|
142
|
+
)
|
|
143
|
+
on conflict (
|
|
144
|
+
${conflict}
|
|
145
|
+
)
|
|
146
|
+
do update set
|
|
147
|
+
${properties.map((x) => `"${x}" = :${x}`).join(",")}
|
|
148
|
+
;`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Returns an (yesql formatted) upsert function with timestamp protection.
|
|
152
|
+
*
|
|
153
|
+
* The WHERE clause in ON CONFLICT DO UPDATE only applies to the conflicting row
|
|
154
|
+
* (the row being updated), not to all rows in the table. PostgreSQL ensures that
|
|
155
|
+
* the condition is evaluated only for the specific row that conflicts with the INSERT.
|
|
156
|
+
*
|
|
157
|
+
*
|
|
158
|
+
* eg:
|
|
159
|
+
* INSERT INTO "stripe"."charges" (
|
|
160
|
+
* "id", "amount", "created", "last_synced_at"
|
|
161
|
+
* )
|
|
162
|
+
* VALUES (
|
|
163
|
+
* :id, :amount, :created, :last_synced_at
|
|
164
|
+
* )
|
|
165
|
+
* ON CONFLICT (id) DO UPDATE SET
|
|
166
|
+
* "amount" = EXCLUDED."amount",
|
|
167
|
+
* "created" = EXCLUDED."created",
|
|
168
|
+
* last_synced_at = :last_synced_at
|
|
169
|
+
* WHERE "charges"."last_synced_at" IS NULL
|
|
170
|
+
* OR "charges"."last_synced_at" < :last_synced_at;
|
|
171
|
+
*/
|
|
172
|
+
constructUpsertWithTimestampProtectionSql = (schema, table, tableSchema) => {
|
|
173
|
+
const conflict = "id";
|
|
174
|
+
const properties = tableSchema.properties;
|
|
175
|
+
return `
|
|
176
|
+
INSERT INTO "${schema}"."${table}" (
|
|
177
|
+
${properties.map((x) => `"${x}"`).join(",")}, "last_synced_at"
|
|
178
|
+
)
|
|
179
|
+
VALUES (
|
|
180
|
+
${properties.map((x) => `:${x}`).join(",")}, :last_synced_at
|
|
181
|
+
)
|
|
182
|
+
ON CONFLICT (${conflict}) DO UPDATE SET
|
|
183
|
+
${properties.filter((x) => x !== "last_synced_at").map((x) => `"${x}" = EXCLUDED."${x}"`).join(",")},
|
|
184
|
+
last_synced_at = :last_synced_at
|
|
185
|
+
WHERE "${table}"."last_synced_at" IS NULL
|
|
186
|
+
OR "${table}"."last_synced_at" < :last_synced_at;`;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* For array object field like invoice.custom_fields
|
|
190
|
+
* ex: [{"name":"Project name","value":"Test Project"}]
|
|
191
|
+
*
|
|
192
|
+
* we need to stringify it first cos passing array object directly will end up with
|
|
193
|
+
* {
|
|
194
|
+
* invalid input syntax for type json
|
|
195
|
+
* detail: 'Expected ":", but found "}".',
|
|
196
|
+
* where: 'JSON data, line 1: ...\\":\\"Project name\\",\\"value\\":\\"Test Project\\"}"}',
|
|
197
|
+
* }
|
|
198
|
+
*/
|
|
199
|
+
cleanseArrayField(obj) {
|
|
200
|
+
const cleansed = { ...obj };
|
|
201
|
+
Object.keys(cleansed).map((k) => {
|
|
202
|
+
const data = cleansed[k];
|
|
203
|
+
if (Array.isArray(data)) {
|
|
204
|
+
cleansed[k] = JSON.stringify(data);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return cleansed;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/schemas/charge.ts
|
|
212
|
+
var chargeSchema = {
|
|
213
|
+
properties: [
|
|
214
|
+
"id",
|
|
215
|
+
"object",
|
|
216
|
+
"paid",
|
|
217
|
+
"order",
|
|
218
|
+
"amount",
|
|
219
|
+
"review",
|
|
220
|
+
"source",
|
|
221
|
+
"status",
|
|
222
|
+
"created",
|
|
223
|
+
"dispute",
|
|
224
|
+
"invoice",
|
|
225
|
+
"outcome",
|
|
226
|
+
"refunds",
|
|
227
|
+
"captured",
|
|
228
|
+
"currency",
|
|
229
|
+
"customer",
|
|
230
|
+
"livemode",
|
|
231
|
+
"metadata",
|
|
232
|
+
"refunded",
|
|
233
|
+
"shipping",
|
|
234
|
+
"application",
|
|
235
|
+
"description",
|
|
236
|
+
"destination",
|
|
237
|
+
"failure_code",
|
|
238
|
+
"on_behalf_of",
|
|
239
|
+
"fraud_details",
|
|
240
|
+
"receipt_email",
|
|
241
|
+
"payment_intent",
|
|
242
|
+
"receipt_number",
|
|
243
|
+
"transfer_group",
|
|
244
|
+
"amount_refunded",
|
|
245
|
+
"application_fee",
|
|
246
|
+
"failure_message",
|
|
247
|
+
"source_transfer",
|
|
248
|
+
"balance_transaction",
|
|
249
|
+
"statement_descriptor",
|
|
250
|
+
"payment_method_details"
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/schemas/checkout_sessions.ts
|
|
255
|
+
var checkoutSessionSchema = {
|
|
256
|
+
properties: [
|
|
257
|
+
"id",
|
|
258
|
+
"object",
|
|
259
|
+
"adaptive_pricing",
|
|
260
|
+
"after_expiration",
|
|
261
|
+
"allow_promotion_codes",
|
|
262
|
+
"amount_subtotal",
|
|
263
|
+
"amount_total",
|
|
264
|
+
"automatic_tax",
|
|
265
|
+
"billing_address_collection",
|
|
266
|
+
"cancel_url",
|
|
267
|
+
"client_reference_id",
|
|
268
|
+
"client_secret",
|
|
269
|
+
"collected_information",
|
|
270
|
+
"consent",
|
|
271
|
+
"consent_collection",
|
|
272
|
+
"created",
|
|
273
|
+
"currency",
|
|
274
|
+
"currency_conversion",
|
|
275
|
+
"custom_fields",
|
|
276
|
+
"custom_text",
|
|
277
|
+
"customer",
|
|
278
|
+
"customer_creation",
|
|
279
|
+
"customer_details",
|
|
280
|
+
"customer_email",
|
|
281
|
+
"discounts",
|
|
282
|
+
"expires_at",
|
|
283
|
+
"invoice",
|
|
284
|
+
"invoice_creation",
|
|
285
|
+
"livemode",
|
|
286
|
+
"locale",
|
|
287
|
+
"metadata",
|
|
288
|
+
"mode",
|
|
289
|
+
"optional_items",
|
|
290
|
+
"payment_intent",
|
|
291
|
+
"payment_link",
|
|
292
|
+
"payment_method_collection",
|
|
293
|
+
"payment_method_configuration_details",
|
|
294
|
+
"payment_method_options",
|
|
295
|
+
"payment_method_types",
|
|
296
|
+
"payment_status",
|
|
297
|
+
"permissions",
|
|
298
|
+
"phone_number_collection",
|
|
299
|
+
"presentment_details",
|
|
300
|
+
"recovered_from",
|
|
301
|
+
"redirect_on_completion",
|
|
302
|
+
"return_url",
|
|
303
|
+
"saved_payment_method_options",
|
|
304
|
+
"setup_intent",
|
|
305
|
+
"shipping_address_collection",
|
|
306
|
+
"shipping_cost",
|
|
307
|
+
"shipping_details",
|
|
308
|
+
"shipping_options",
|
|
309
|
+
"status",
|
|
310
|
+
"submit_type",
|
|
311
|
+
"subscription",
|
|
312
|
+
"success_url",
|
|
313
|
+
"tax_id_collection",
|
|
314
|
+
"total_details",
|
|
315
|
+
"ui_mode",
|
|
316
|
+
"url",
|
|
317
|
+
"wallet_options"
|
|
318
|
+
]
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/schemas/checkout_session_line_items.ts
|
|
322
|
+
var checkoutSessionLineItemSchema = {
|
|
323
|
+
properties: [
|
|
324
|
+
"id",
|
|
325
|
+
"object",
|
|
326
|
+
"amount_discount",
|
|
327
|
+
"amount_subtotal",
|
|
328
|
+
"amount_tax",
|
|
329
|
+
"amount_total",
|
|
330
|
+
"currency",
|
|
331
|
+
"description",
|
|
332
|
+
"price",
|
|
333
|
+
"quantity",
|
|
334
|
+
"checkout_session"
|
|
335
|
+
]
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/schemas/credit_note.ts
|
|
339
|
+
var creditNoteSchema = {
|
|
340
|
+
properties: [
|
|
341
|
+
"id",
|
|
342
|
+
"object",
|
|
343
|
+
"amount",
|
|
344
|
+
"amount_shipping",
|
|
345
|
+
"created",
|
|
346
|
+
"currency",
|
|
347
|
+
"customer",
|
|
348
|
+
"customer_balance_transaction",
|
|
349
|
+
"discount_amount",
|
|
350
|
+
"discount_amounts",
|
|
351
|
+
"invoice",
|
|
352
|
+
"lines",
|
|
353
|
+
"livemode",
|
|
354
|
+
"memo",
|
|
355
|
+
"metadata",
|
|
356
|
+
"number",
|
|
357
|
+
"out_of_band_amount",
|
|
358
|
+
"pdf",
|
|
359
|
+
"reason",
|
|
360
|
+
"refund",
|
|
361
|
+
"shipping_cost",
|
|
362
|
+
"status",
|
|
363
|
+
"subtotal",
|
|
364
|
+
"subtotal_excluding_tax",
|
|
365
|
+
"tax_amounts",
|
|
366
|
+
"total",
|
|
367
|
+
"total_excluding_tax",
|
|
368
|
+
"type",
|
|
369
|
+
"voided_at"
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/schemas/customer.ts
|
|
374
|
+
var customerSchema = {
|
|
375
|
+
properties: [
|
|
376
|
+
"id",
|
|
377
|
+
"object",
|
|
378
|
+
"address",
|
|
379
|
+
"description",
|
|
380
|
+
"email",
|
|
381
|
+
"metadata",
|
|
382
|
+
"name",
|
|
383
|
+
"phone",
|
|
384
|
+
"shipping",
|
|
385
|
+
"balance",
|
|
386
|
+
"created",
|
|
387
|
+
"currency",
|
|
388
|
+
"default_source",
|
|
389
|
+
"delinquent",
|
|
390
|
+
"discount",
|
|
391
|
+
"invoice_prefix",
|
|
392
|
+
"invoice_settings",
|
|
393
|
+
"livemode",
|
|
394
|
+
"next_invoice_sequence",
|
|
395
|
+
"preferred_locales",
|
|
396
|
+
"tax_exempt"
|
|
397
|
+
]
|
|
398
|
+
};
|
|
399
|
+
var customerDeletedSchema = {
|
|
400
|
+
properties: ["id", "object", "deleted"]
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/schemas/dispute.ts
|
|
404
|
+
var disputeSchema = {
|
|
405
|
+
properties: [
|
|
406
|
+
"id",
|
|
407
|
+
"object",
|
|
408
|
+
"amount",
|
|
409
|
+
"charge",
|
|
410
|
+
"created",
|
|
411
|
+
"currency",
|
|
412
|
+
"balance_transactions",
|
|
413
|
+
"evidence",
|
|
414
|
+
"evidence_details",
|
|
415
|
+
"is_charge_refundable",
|
|
416
|
+
"livemode",
|
|
417
|
+
"metadata",
|
|
418
|
+
"payment_intent",
|
|
419
|
+
"reason",
|
|
420
|
+
"status"
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/schemas/invoice.ts
|
|
425
|
+
var invoiceSchema = {
|
|
426
|
+
properties: [
|
|
427
|
+
"id",
|
|
428
|
+
"object",
|
|
429
|
+
"auto_advance",
|
|
430
|
+
"collection_method",
|
|
431
|
+
"currency",
|
|
432
|
+
"description",
|
|
433
|
+
"hosted_invoice_url",
|
|
434
|
+
"lines",
|
|
435
|
+
"metadata",
|
|
436
|
+
"period_end",
|
|
437
|
+
"period_start",
|
|
438
|
+
"status",
|
|
439
|
+
"total",
|
|
440
|
+
"account_country",
|
|
441
|
+
"account_name",
|
|
442
|
+
"account_tax_ids",
|
|
443
|
+
"amount_due",
|
|
444
|
+
"amount_paid",
|
|
445
|
+
"amount_remaining",
|
|
446
|
+
"application_fee_amount",
|
|
447
|
+
"attempt_count",
|
|
448
|
+
"attempted",
|
|
449
|
+
"billing_reason",
|
|
450
|
+
"created",
|
|
451
|
+
"custom_fields",
|
|
452
|
+
"customer_address",
|
|
453
|
+
"customer_email",
|
|
454
|
+
"customer_name",
|
|
455
|
+
"customer_phone",
|
|
456
|
+
"customer_shipping",
|
|
457
|
+
"customer_tax_exempt",
|
|
458
|
+
"customer_tax_ids",
|
|
459
|
+
"default_tax_rates",
|
|
460
|
+
"discount",
|
|
461
|
+
"discounts",
|
|
462
|
+
"due_date",
|
|
463
|
+
"ending_balance",
|
|
464
|
+
"footer",
|
|
465
|
+
"invoice_pdf",
|
|
466
|
+
"last_finalization_error",
|
|
467
|
+
"livemode",
|
|
468
|
+
"next_payment_attempt",
|
|
469
|
+
"number",
|
|
470
|
+
"paid",
|
|
471
|
+
"payment_settings",
|
|
472
|
+
"post_payment_credit_notes_amount",
|
|
473
|
+
"pre_payment_credit_notes_amount",
|
|
474
|
+
"receipt_number",
|
|
475
|
+
"starting_balance",
|
|
476
|
+
"statement_descriptor",
|
|
477
|
+
"status_transitions",
|
|
478
|
+
"subtotal",
|
|
479
|
+
"tax",
|
|
480
|
+
"total_discount_amounts",
|
|
481
|
+
"total_tax_amounts",
|
|
482
|
+
"transfer_data",
|
|
483
|
+
"webhooks_delivered_at",
|
|
484
|
+
"customer",
|
|
485
|
+
"subscription",
|
|
486
|
+
"payment_intent",
|
|
487
|
+
"default_payment_method",
|
|
488
|
+
"default_source",
|
|
489
|
+
"on_behalf_of",
|
|
490
|
+
"charge"
|
|
491
|
+
]
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/schemas/plan.ts
|
|
495
|
+
var planSchema = {
|
|
496
|
+
properties: [
|
|
497
|
+
"id",
|
|
498
|
+
"object",
|
|
499
|
+
"active",
|
|
500
|
+
"amount",
|
|
501
|
+
"created",
|
|
502
|
+
"product",
|
|
503
|
+
"currency",
|
|
504
|
+
"interval",
|
|
505
|
+
"livemode",
|
|
506
|
+
"metadata",
|
|
507
|
+
"nickname",
|
|
508
|
+
"tiers_mode",
|
|
509
|
+
"usage_type",
|
|
510
|
+
"billing_scheme",
|
|
511
|
+
"interval_count",
|
|
512
|
+
"aggregate_usage",
|
|
513
|
+
"transform_usage",
|
|
514
|
+
"trial_period_days"
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// src/schemas/price.ts
|
|
519
|
+
var priceSchema = {
|
|
520
|
+
properties: [
|
|
521
|
+
"id",
|
|
522
|
+
"object",
|
|
523
|
+
"active",
|
|
524
|
+
"currency",
|
|
525
|
+
"metadata",
|
|
526
|
+
"nickname",
|
|
527
|
+
"recurring",
|
|
528
|
+
"type",
|
|
529
|
+
"unit_amount",
|
|
530
|
+
"billing_scheme",
|
|
531
|
+
"created",
|
|
532
|
+
"livemode",
|
|
533
|
+
"lookup_key",
|
|
534
|
+
"tiers_mode",
|
|
535
|
+
"transform_quantity",
|
|
536
|
+
"unit_amount_decimal",
|
|
537
|
+
"product"
|
|
538
|
+
]
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// src/schemas/product.ts
|
|
542
|
+
var productSchema = {
|
|
543
|
+
properties: [
|
|
544
|
+
"id",
|
|
545
|
+
"object",
|
|
546
|
+
"active",
|
|
547
|
+
"default_price",
|
|
548
|
+
"description",
|
|
549
|
+
"metadata",
|
|
550
|
+
"name",
|
|
551
|
+
"created",
|
|
552
|
+
"images",
|
|
553
|
+
"marketing_features",
|
|
554
|
+
"livemode",
|
|
555
|
+
"package_dimensions",
|
|
556
|
+
"shippable",
|
|
557
|
+
"statement_descriptor",
|
|
558
|
+
"unit_label",
|
|
559
|
+
"updated",
|
|
560
|
+
"url"
|
|
561
|
+
]
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/schemas/payment_intent.ts
|
|
565
|
+
var paymentIntentSchema = {
|
|
566
|
+
properties: [
|
|
567
|
+
"id",
|
|
568
|
+
"object",
|
|
569
|
+
"amount",
|
|
570
|
+
"amount_capturable",
|
|
571
|
+
"amount_details",
|
|
572
|
+
"amount_received",
|
|
573
|
+
"application",
|
|
574
|
+
"application_fee_amount",
|
|
575
|
+
"automatic_payment_methods",
|
|
576
|
+
"canceled_at",
|
|
577
|
+
"cancellation_reason",
|
|
578
|
+
"capture_method",
|
|
579
|
+
"client_secret",
|
|
580
|
+
"confirmation_method",
|
|
581
|
+
"created",
|
|
582
|
+
"currency",
|
|
583
|
+
"customer",
|
|
584
|
+
"description",
|
|
585
|
+
"invoice",
|
|
586
|
+
"last_payment_error",
|
|
587
|
+
"livemode",
|
|
588
|
+
"metadata",
|
|
589
|
+
"next_action",
|
|
590
|
+
"on_behalf_of",
|
|
591
|
+
"payment_method",
|
|
592
|
+
"payment_method_options",
|
|
593
|
+
"payment_method_types",
|
|
594
|
+
"processing",
|
|
595
|
+
"receipt_email",
|
|
596
|
+
"review",
|
|
597
|
+
"setup_future_usage",
|
|
598
|
+
"shipping",
|
|
599
|
+
"statement_descriptor",
|
|
600
|
+
"statement_descriptor_suffix",
|
|
601
|
+
"status",
|
|
602
|
+
"transfer_data",
|
|
603
|
+
"transfer_group"
|
|
604
|
+
]
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/schemas/payment_methods.ts
|
|
608
|
+
var paymentMethodsSchema = {
|
|
609
|
+
properties: [
|
|
610
|
+
"id",
|
|
611
|
+
"object",
|
|
612
|
+
"created",
|
|
613
|
+
"customer",
|
|
614
|
+
"type",
|
|
615
|
+
"billing_details",
|
|
616
|
+
"metadata",
|
|
617
|
+
"card"
|
|
618
|
+
]
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/schemas/setup_intents.ts
|
|
622
|
+
var setupIntentsSchema = {
|
|
623
|
+
properties: [
|
|
624
|
+
"id",
|
|
625
|
+
"object",
|
|
626
|
+
"created",
|
|
627
|
+
"customer",
|
|
628
|
+
"description",
|
|
629
|
+
"payment_method",
|
|
630
|
+
"status",
|
|
631
|
+
"usage",
|
|
632
|
+
"cancellation_reason",
|
|
633
|
+
"latest_attempt",
|
|
634
|
+
"mandate",
|
|
635
|
+
"single_use_mandate",
|
|
636
|
+
"on_behalf_of"
|
|
637
|
+
]
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/schemas/tax_id.ts
|
|
641
|
+
var taxIdSchema = {
|
|
642
|
+
properties: [
|
|
643
|
+
"id",
|
|
644
|
+
"country",
|
|
645
|
+
"customer",
|
|
646
|
+
"type",
|
|
647
|
+
"value",
|
|
648
|
+
"object",
|
|
649
|
+
"created",
|
|
650
|
+
"livemode",
|
|
651
|
+
"owner"
|
|
652
|
+
]
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/schemas/subscription_item.ts
|
|
656
|
+
var subscriptionItemSchema = {
|
|
657
|
+
properties: [
|
|
658
|
+
"id",
|
|
659
|
+
"object",
|
|
660
|
+
"billing_thresholds",
|
|
661
|
+
"created",
|
|
662
|
+
"deleted",
|
|
663
|
+
"metadata",
|
|
664
|
+
"quantity",
|
|
665
|
+
"price",
|
|
666
|
+
"subscription",
|
|
667
|
+
"tax_rates",
|
|
668
|
+
"current_period_end",
|
|
669
|
+
"current_period_start"
|
|
670
|
+
]
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// src/schemas/subscription_schedules.ts
|
|
674
|
+
var subscriptionScheduleSchema = {
|
|
675
|
+
properties: [
|
|
676
|
+
"id",
|
|
677
|
+
"object",
|
|
678
|
+
"application",
|
|
679
|
+
"canceled_at",
|
|
680
|
+
"completed_at",
|
|
681
|
+
"created",
|
|
682
|
+
"current_phase",
|
|
683
|
+
"customer",
|
|
684
|
+
"default_settings",
|
|
685
|
+
"end_behavior",
|
|
686
|
+
"livemode",
|
|
687
|
+
"metadata",
|
|
688
|
+
"phases",
|
|
689
|
+
"released_at",
|
|
690
|
+
"released_subscription",
|
|
691
|
+
"status",
|
|
692
|
+
"subscription",
|
|
693
|
+
"test_clock"
|
|
694
|
+
]
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// src/schemas/subscription.ts
|
|
698
|
+
var subscriptionSchema = {
|
|
699
|
+
properties: [
|
|
700
|
+
"id",
|
|
701
|
+
"object",
|
|
702
|
+
"cancel_at_period_end",
|
|
703
|
+
"current_period_end",
|
|
704
|
+
"current_period_start",
|
|
705
|
+
"default_payment_method",
|
|
706
|
+
"items",
|
|
707
|
+
"metadata",
|
|
708
|
+
"pending_setup_intent",
|
|
709
|
+
"pending_update",
|
|
710
|
+
"status",
|
|
711
|
+
"application_fee_percent",
|
|
712
|
+
"billing_cycle_anchor",
|
|
713
|
+
"billing_thresholds",
|
|
714
|
+
"cancel_at",
|
|
715
|
+
"canceled_at",
|
|
716
|
+
"collection_method",
|
|
717
|
+
"created",
|
|
718
|
+
"days_until_due",
|
|
719
|
+
"default_source",
|
|
720
|
+
"default_tax_rates",
|
|
721
|
+
"discount",
|
|
722
|
+
"ended_at",
|
|
723
|
+
"livemode",
|
|
724
|
+
"next_pending_invoice_item_invoice",
|
|
725
|
+
"pause_collection",
|
|
726
|
+
"pending_invoice_item_interval",
|
|
727
|
+
"start_date",
|
|
728
|
+
"transfer_data",
|
|
729
|
+
"trial_end",
|
|
730
|
+
"trial_start",
|
|
731
|
+
"schedule",
|
|
732
|
+
"customer",
|
|
733
|
+
"latest_invoice",
|
|
734
|
+
"plan"
|
|
735
|
+
]
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// src/schemas/early_fraud_warning.ts
|
|
739
|
+
var earlyFraudWarningSchema = {
|
|
740
|
+
properties: [
|
|
741
|
+
"id",
|
|
742
|
+
"object",
|
|
743
|
+
"actionable",
|
|
744
|
+
"charge",
|
|
745
|
+
"created",
|
|
746
|
+
"fraud_type",
|
|
747
|
+
"livemode",
|
|
748
|
+
"payment_intent"
|
|
749
|
+
]
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// src/schemas/review.ts
|
|
753
|
+
var reviewSchema = {
|
|
754
|
+
properties: [
|
|
755
|
+
"id",
|
|
756
|
+
"object",
|
|
757
|
+
"billing_zip",
|
|
758
|
+
"created",
|
|
759
|
+
"charge",
|
|
760
|
+
"closed_reason",
|
|
761
|
+
"livemode",
|
|
762
|
+
"ip_address",
|
|
763
|
+
"ip_address_location",
|
|
764
|
+
"open",
|
|
765
|
+
"opened_reason",
|
|
766
|
+
"payment_intent",
|
|
767
|
+
"reason",
|
|
768
|
+
"session"
|
|
769
|
+
]
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/schemas/refund.ts
|
|
773
|
+
var refundSchema = {
|
|
774
|
+
properties: [
|
|
775
|
+
"id",
|
|
776
|
+
"object",
|
|
777
|
+
"amount",
|
|
778
|
+
"balance_transaction",
|
|
779
|
+
"charge",
|
|
780
|
+
"created",
|
|
781
|
+
"currency",
|
|
782
|
+
"destination_details",
|
|
783
|
+
"metadata",
|
|
784
|
+
"payment_intent",
|
|
785
|
+
"reason",
|
|
786
|
+
"receipt_number",
|
|
787
|
+
"source_transfer_reversal",
|
|
788
|
+
"status",
|
|
789
|
+
"transfer_reversal"
|
|
790
|
+
]
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/schemas/active_entitlement.ts
|
|
794
|
+
var activeEntitlementSchema = {
|
|
795
|
+
properties: ["id", "object", "feature", "lookup_key", "livemode", "customer"]
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// src/schemas/feature.ts
|
|
799
|
+
var featureSchema = {
|
|
800
|
+
properties: ["id", "object", "livemode", "name", "lookup_key", "active", "metadata"]
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/schemas/managed_webhook.ts
|
|
804
|
+
var managedWebhookSchema = {
|
|
805
|
+
properties: [
|
|
806
|
+
"id",
|
|
807
|
+
"object",
|
|
808
|
+
"uuid",
|
|
809
|
+
"url",
|
|
810
|
+
"enabled_events",
|
|
811
|
+
"description",
|
|
812
|
+
"enabled",
|
|
813
|
+
"livemode",
|
|
814
|
+
"metadata",
|
|
815
|
+
"secret",
|
|
816
|
+
"status",
|
|
817
|
+
"api_version",
|
|
818
|
+
"created"
|
|
819
|
+
]
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// src/stripeSync.ts
|
|
823
|
+
var import_node_crypto = require("crypto");
|
|
824
|
+
|
|
825
|
+
// src/database/migrate.ts
|
|
826
|
+
var import_pg2 = require("pg");
|
|
827
|
+
var import_pg_node_migrations = require("pg-node-migrations");
|
|
828
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
829
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
830
|
+
var import_node_url = require("url");
|
|
831
|
+
var __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
832
|
+
var __dirname = import_node_path.default.dirname(__filename2);
|
|
833
|
+
async function connectAndMigrate(client, migrationsDirectory, config, logOnError = false) {
|
|
834
|
+
if (!import_node_fs.default.existsSync(migrationsDirectory)) {
|
|
835
|
+
config.logger?.info(`Migrations directory ${migrationsDirectory} not found, skipping`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const optionalConfig = {
|
|
839
|
+
schemaName: config.schema,
|
|
840
|
+
tableName: "migrations"
|
|
841
|
+
};
|
|
842
|
+
try {
|
|
843
|
+
await (0, import_pg_node_migrations.migrate)({ client }, migrationsDirectory, optionalConfig);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (logOnError && error instanceof Error) {
|
|
846
|
+
config.logger?.error(error, "Migration error:");
|
|
847
|
+
} else {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function runMigrations(config) {
|
|
853
|
+
const client = new import_pg2.Client({
|
|
854
|
+
connectionString: config.databaseUrl,
|
|
855
|
+
ssl: config.ssl,
|
|
856
|
+
connectionTimeoutMillis: 1e4
|
|
857
|
+
});
|
|
858
|
+
try {
|
|
859
|
+
await client.connect();
|
|
860
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${config.schema};`);
|
|
861
|
+
config.logger?.info("Running migrations");
|
|
862
|
+
await connectAndMigrate(client, import_node_path.default.resolve(__dirname, "./migrations"), config);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
config.logger?.error(err, "Error running migrations");
|
|
865
|
+
throw err;
|
|
866
|
+
} finally {
|
|
867
|
+
await client.end();
|
|
868
|
+
config.logger?.info("Finished migrations");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/stripeSync.ts
|
|
873
|
+
var import_express = __toESM(require("express"), 1);
|
|
874
|
+
function getUniqueIds(entries, key) {
|
|
875
|
+
const set = new Set(
|
|
876
|
+
entries.map((subscription) => subscription?.[key]?.toString()).filter((it) => Boolean(it))
|
|
877
|
+
);
|
|
878
|
+
return Array.from(set);
|
|
879
|
+
}
|
|
880
|
+
var DEFAULT_SCHEMA = "stripe";
|
|
881
|
+
var StripeAutoSync = class {
|
|
882
|
+
options;
|
|
883
|
+
webhookId = null;
|
|
884
|
+
webhookUuid = null;
|
|
885
|
+
stripeSync = null;
|
|
886
|
+
constructor(options) {
|
|
887
|
+
this.options = {
|
|
888
|
+
...options,
|
|
889
|
+
// Apply defaults for undefined values
|
|
890
|
+
schema: options.schema || "stripe",
|
|
891
|
+
webhookPath: options.webhookPath || "/stripe-webhooks",
|
|
892
|
+
stripeApiVersion: options.stripeApiVersion || "2020-08-27",
|
|
893
|
+
autoExpandLists: options.autoExpandLists !== void 0 ? options.autoExpandLists : false,
|
|
894
|
+
backfillRelatedEntities: options.backfillRelatedEntities !== void 0 ? options.backfillRelatedEntities : true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Starts the Stripe Sync infrastructure and mounts webhook handler:
|
|
899
|
+
* 1. Runs database migrations
|
|
900
|
+
* 2. Creates StripeSync instance
|
|
901
|
+
* 3. Creates managed webhook endpoint
|
|
902
|
+
* 4. Mounts webhook handler on provided Express app
|
|
903
|
+
* 5. Applies body parsing middleware (automatically skips webhook routes)
|
|
904
|
+
*
|
|
905
|
+
* @param app - Express app to mount webhook handler on
|
|
906
|
+
* @returns Information about the running instance
|
|
907
|
+
*/
|
|
908
|
+
async start(app) {
|
|
909
|
+
try {
|
|
910
|
+
await runMigrations({
|
|
911
|
+
databaseUrl: this.options.databaseUrl,
|
|
912
|
+
schema: this.options.schema
|
|
913
|
+
});
|
|
914
|
+
const poolConfig = {
|
|
915
|
+
max: 10,
|
|
916
|
+
connectionString: this.options.databaseUrl,
|
|
917
|
+
keepAlive: true
|
|
918
|
+
};
|
|
919
|
+
this.stripeSync = new StripeSync({
|
|
920
|
+
databaseUrl: this.options.databaseUrl,
|
|
921
|
+
schema: this.options.schema,
|
|
922
|
+
stripeSecretKey: this.options.stripeApiKey,
|
|
923
|
+
stripeApiVersion: this.options.stripeApiVersion,
|
|
924
|
+
autoExpandLists: this.options.autoExpandLists,
|
|
925
|
+
backfillRelatedEntities: this.options.backfillRelatedEntities,
|
|
926
|
+
poolConfig
|
|
927
|
+
});
|
|
928
|
+
const baseUrl = this.options.baseUrl();
|
|
929
|
+
const { webhook, uuid } = await this.stripeSync.createManagedWebhook(
|
|
930
|
+
`${baseUrl}${this.options.webhookPath}`,
|
|
931
|
+
{
|
|
932
|
+
enabled_events: ["*"],
|
|
933
|
+
// Subscribe to all events
|
|
934
|
+
description: "stripe-sync-cli development webhook"
|
|
935
|
+
}
|
|
936
|
+
);
|
|
937
|
+
this.webhookId = webhook.id;
|
|
938
|
+
this.webhookUuid = uuid;
|
|
939
|
+
this.mountWebhook(app);
|
|
940
|
+
app.use(this.getBodyParserMiddleware());
|
|
941
|
+
return {
|
|
942
|
+
baseUrl,
|
|
943
|
+
webhookUrl: webhook.url,
|
|
944
|
+
webhookUuid: uuid
|
|
945
|
+
};
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error instanceof Error) {
|
|
948
|
+
console.error("Failed to start Stripe Sync:", error.message);
|
|
949
|
+
console.error(error.stack || "");
|
|
950
|
+
} else {
|
|
951
|
+
console.error("Failed to start Stripe Sync:", String(error));
|
|
952
|
+
}
|
|
953
|
+
await this.stop();
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Stops all services and cleans up resources:
|
|
959
|
+
* 1. Deletes Stripe webhook endpoint from Stripe and database
|
|
960
|
+
*/
|
|
961
|
+
async stop() {
|
|
962
|
+
if (this.webhookId && this.stripeSync) {
|
|
963
|
+
try {
|
|
964
|
+
await this.stripeSync.deleteManagedWebhook(this.webhookId);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.error("Could not delete webhook:", error);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Returns Express middleware for body parsing that automatically skips webhook routes.
|
|
972
|
+
* This middleware applies JSON and URL-encoded parsers to all routes EXCEPT the webhook path,
|
|
973
|
+
* which needs raw body for signature verification.
|
|
974
|
+
*
|
|
975
|
+
* @returns Express middleware function
|
|
976
|
+
*/
|
|
977
|
+
getBodyParserMiddleware() {
|
|
978
|
+
const webhookPath = this.options.webhookPath;
|
|
979
|
+
return (req, res, next) => {
|
|
980
|
+
if (req.path.startsWith(webhookPath)) {
|
|
981
|
+
return next();
|
|
982
|
+
}
|
|
983
|
+
import_express.default.json()(req, res, (err) => {
|
|
984
|
+
if (err) return next(err);
|
|
985
|
+
import_express.default.urlencoded({ extended: false })(req, res, next);
|
|
986
|
+
});
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Mounts the Stripe webhook handler on the provided Express app.
|
|
991
|
+
* Applies raw body parser middleware for signature verification.
|
|
992
|
+
* IMPORTANT: Must be called BEFORE app.use(express.json()) to ensure raw body parsing.
|
|
993
|
+
*/
|
|
994
|
+
mountWebhook(app) {
|
|
995
|
+
const webhookRoute = `${this.options.webhookPath}/:uuid`;
|
|
996
|
+
app.use(webhookRoute, import_express.default.raw({ type: "application/json" }));
|
|
997
|
+
app.post(webhookRoute, async (req, res) => {
|
|
998
|
+
const sig = req.headers["stripe-signature"];
|
|
999
|
+
if (!sig || typeof sig !== "string") {
|
|
1000
|
+
console.error("[Webhook] Missing stripe-signature header");
|
|
1001
|
+
return res.status(400).send({ error: "Missing stripe-signature header" });
|
|
1002
|
+
}
|
|
1003
|
+
const { uuid } = req.params;
|
|
1004
|
+
const rawBody = req.body;
|
|
1005
|
+
if (!rawBody || !Buffer.isBuffer(rawBody)) {
|
|
1006
|
+
console.error("[Webhook] Body is not a Buffer!", {
|
|
1007
|
+
hasBody: !!rawBody,
|
|
1008
|
+
bodyType: typeof rawBody,
|
|
1009
|
+
isBuffer: Buffer.isBuffer(rawBody),
|
|
1010
|
+
bodyConstructor: rawBody?.constructor?.name
|
|
1011
|
+
});
|
|
1012
|
+
return res.status(400).send({ error: "Missing raw body for signature verification" });
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
await this.stripeSync.processWebhook(rawBody, sig, uuid);
|
|
1016
|
+
return res.status(200).send({ received: true });
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.error("[Webhook] Processing error:", error.message);
|
|
1019
|
+
return res.status(400).send({ error: error.message });
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
var StripeSync = class {
|
|
1025
|
+
constructor(config) {
|
|
1026
|
+
this.config = config;
|
|
1027
|
+
this.stripe = new import_stripe.default(config.stripeSecretKey, {
|
|
1028
|
+
// https://github.com/stripe/stripe-node#configuration
|
|
1029
|
+
// @ts-ignore
|
|
1030
|
+
apiVersion: config.stripeApiVersion,
|
|
1031
|
+
appInfo: {
|
|
1032
|
+
name: "Stripe Postgres Sync"
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
this.config.logger?.info(
|
|
1036
|
+
{ autoExpandLists: config.autoExpandLists, stripeApiVersion: config.stripeApiVersion },
|
|
1037
|
+
"StripeSync initialized"
|
|
1038
|
+
);
|
|
1039
|
+
const poolConfig = config.poolConfig ?? {};
|
|
1040
|
+
if (config.databaseUrl) {
|
|
1041
|
+
poolConfig.connectionString = config.databaseUrl;
|
|
1042
|
+
}
|
|
1043
|
+
if (config.maxPostgresConnections) {
|
|
1044
|
+
poolConfig.max = config.maxPostgresConnections;
|
|
1045
|
+
}
|
|
1046
|
+
if (poolConfig.max === void 0) {
|
|
1047
|
+
poolConfig.max = 10;
|
|
1048
|
+
}
|
|
1049
|
+
if (poolConfig.keepAlive === void 0) {
|
|
1050
|
+
poolConfig.keepAlive = true;
|
|
1051
|
+
}
|
|
1052
|
+
this.postgresClient = new PostgresClient({
|
|
1053
|
+
schema: config.schema || DEFAULT_SCHEMA,
|
|
1054
|
+
poolConfig
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
stripe;
|
|
1058
|
+
postgresClient;
|
|
1059
|
+
async processWebhook(payload, signature, uuid) {
|
|
1060
|
+
const result = await this.postgresClient.query(
|
|
1061
|
+
`SELECT secret FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" WHERE uuid = $1`,
|
|
1062
|
+
[uuid]
|
|
1063
|
+
);
|
|
1064
|
+
if (result.rows.length === 0) {
|
|
1065
|
+
throw new Error(`No managed webhook found with UUID: ${uuid}`);
|
|
1066
|
+
}
|
|
1067
|
+
const webhookSecret = result.rows[0].secret;
|
|
1068
|
+
const event = await this.stripe.webhooks.constructEventAsync(
|
|
1069
|
+
payload,
|
|
1070
|
+
signature,
|
|
1071
|
+
webhookSecret
|
|
1072
|
+
);
|
|
1073
|
+
return this.processEvent(event);
|
|
1074
|
+
}
|
|
1075
|
+
async processEvent(event) {
|
|
1076
|
+
switch (event.type) {
|
|
1077
|
+
case "charge.captured":
|
|
1078
|
+
case "charge.expired":
|
|
1079
|
+
case "charge.failed":
|
|
1080
|
+
case "charge.pending":
|
|
1081
|
+
case "charge.refunded":
|
|
1082
|
+
case "charge.succeeded":
|
|
1083
|
+
case "charge.updated": {
|
|
1084
|
+
const { entity: charge, refetched } = await this.fetchOrUseWebhookData(
|
|
1085
|
+
event.data.object,
|
|
1086
|
+
(id) => this.stripe.charges.retrieve(id),
|
|
1087
|
+
(charge2) => charge2.status === "failed" || charge2.status === "succeeded"
|
|
1088
|
+
);
|
|
1089
|
+
this.config.logger?.info(
|
|
1090
|
+
`Received webhook ${event.id}: ${event.type} for charge ${charge.id}`
|
|
1091
|
+
);
|
|
1092
|
+
await this.upsertCharges([charge], false, this.getSyncTimestamp(event, refetched));
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case "customer.deleted": {
|
|
1096
|
+
const customer = {
|
|
1097
|
+
id: event.data.object.id,
|
|
1098
|
+
object: "customer",
|
|
1099
|
+
deleted: true
|
|
1100
|
+
};
|
|
1101
|
+
this.config.logger?.info(
|
|
1102
|
+
`Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
|
|
1103
|
+
);
|
|
1104
|
+
await this.upsertCustomers([customer], this.getSyncTimestamp(event, false));
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "checkout.session.async_payment_failed":
|
|
1108
|
+
case "checkout.session.async_payment_succeeded":
|
|
1109
|
+
case "checkout.session.completed":
|
|
1110
|
+
case "checkout.session.expired": {
|
|
1111
|
+
const { entity: checkoutSession, refetched } = await this.fetchOrUseWebhookData(
|
|
1112
|
+
event.data.object,
|
|
1113
|
+
(id) => this.stripe.checkout.sessions.retrieve(id)
|
|
1114
|
+
);
|
|
1115
|
+
this.config.logger?.info(
|
|
1116
|
+
`Received webhook ${event.id}: ${event.type} for checkout session ${checkoutSession.id}`
|
|
1117
|
+
);
|
|
1118
|
+
await this.upsertCheckoutSessions(
|
|
1119
|
+
[checkoutSession],
|
|
1120
|
+
false,
|
|
1121
|
+
this.getSyncTimestamp(event, refetched)
|
|
1122
|
+
);
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
case "customer.created":
|
|
1126
|
+
case "customer.updated": {
|
|
1127
|
+
const { entity: customer, refetched } = await this.fetchOrUseWebhookData(
|
|
1128
|
+
event.data.object,
|
|
1129
|
+
(id) => this.stripe.customers.retrieve(id),
|
|
1130
|
+
(customer2) => customer2.deleted === true
|
|
1131
|
+
);
|
|
1132
|
+
this.config.logger?.info(
|
|
1133
|
+
`Received webhook ${event.id}: ${event.type} for customer ${customer.id}`
|
|
1134
|
+
);
|
|
1135
|
+
await this.upsertCustomers([customer], this.getSyncTimestamp(event, refetched));
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
case "customer.subscription.created":
|
|
1139
|
+
case "customer.subscription.deleted":
|
|
1140
|
+
// Soft delete using `status = canceled`
|
|
1141
|
+
case "customer.subscription.paused":
|
|
1142
|
+
case "customer.subscription.pending_update_applied":
|
|
1143
|
+
case "customer.subscription.pending_update_expired":
|
|
1144
|
+
case "customer.subscription.trial_will_end":
|
|
1145
|
+
case "customer.subscription.resumed":
|
|
1146
|
+
case "customer.subscription.updated": {
|
|
1147
|
+
const { entity: subscription, refetched } = await this.fetchOrUseWebhookData(
|
|
1148
|
+
event.data.object,
|
|
1149
|
+
(id) => this.stripe.subscriptions.retrieve(id),
|
|
1150
|
+
(subscription2) => subscription2.status === "canceled" || subscription2.status === "incomplete_expired"
|
|
1151
|
+
);
|
|
1152
|
+
this.config.logger?.info(
|
|
1153
|
+
`Received webhook ${event.id}: ${event.type} for subscription ${subscription.id}`
|
|
1154
|
+
);
|
|
1155
|
+
await this.upsertSubscriptions(
|
|
1156
|
+
[subscription],
|
|
1157
|
+
false,
|
|
1158
|
+
this.getSyncTimestamp(event, refetched)
|
|
1159
|
+
);
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
case "customer.tax_id.updated":
|
|
1163
|
+
case "customer.tax_id.created": {
|
|
1164
|
+
const { entity: taxId, refetched } = await this.fetchOrUseWebhookData(
|
|
1165
|
+
event.data.object,
|
|
1166
|
+
(id) => this.stripe.taxIds.retrieve(id)
|
|
1167
|
+
);
|
|
1168
|
+
this.config.logger?.info(
|
|
1169
|
+
`Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
|
|
1170
|
+
);
|
|
1171
|
+
await this.upsertTaxIds([taxId], false, this.getSyncTimestamp(event, refetched));
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
case "customer.tax_id.deleted": {
|
|
1175
|
+
const taxId = event.data.object;
|
|
1176
|
+
this.config.logger?.info(
|
|
1177
|
+
`Received webhook ${event.id}: ${event.type} for taxId ${taxId.id}`
|
|
1178
|
+
);
|
|
1179
|
+
await this.deleteTaxId(taxId.id);
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
case "invoice.created":
|
|
1183
|
+
case "invoice.deleted":
|
|
1184
|
+
case "invoice.finalized":
|
|
1185
|
+
case "invoice.finalization_failed":
|
|
1186
|
+
case "invoice.paid":
|
|
1187
|
+
case "invoice.payment_action_required":
|
|
1188
|
+
case "invoice.payment_failed":
|
|
1189
|
+
case "invoice.payment_succeeded":
|
|
1190
|
+
case "invoice.upcoming":
|
|
1191
|
+
case "invoice.sent":
|
|
1192
|
+
case "invoice.voided":
|
|
1193
|
+
case "invoice.marked_uncollectible":
|
|
1194
|
+
case "invoice.updated": {
|
|
1195
|
+
const { entity: invoice, refetched } = await this.fetchOrUseWebhookData(
|
|
1196
|
+
event.data.object,
|
|
1197
|
+
(id) => this.stripe.invoices.retrieve(id),
|
|
1198
|
+
(invoice2) => invoice2.status === "void"
|
|
1199
|
+
);
|
|
1200
|
+
this.config.logger?.info(
|
|
1201
|
+
`Received webhook ${event.id}: ${event.type} for invoice ${invoice.id}`
|
|
1202
|
+
);
|
|
1203
|
+
await this.upsertInvoices([invoice], false, this.getSyncTimestamp(event, refetched));
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
case "product.created":
|
|
1207
|
+
case "product.updated": {
|
|
1208
|
+
try {
|
|
1209
|
+
const { entity: product, refetched } = await this.fetchOrUseWebhookData(
|
|
1210
|
+
event.data.object,
|
|
1211
|
+
(id) => this.stripe.products.retrieve(id)
|
|
1212
|
+
);
|
|
1213
|
+
this.config.logger?.info(
|
|
1214
|
+
`Received webhook ${event.id}: ${event.type} for product ${product.id}`
|
|
1215
|
+
);
|
|
1216
|
+
await this.upsertProducts([product], this.getSyncTimestamp(event, refetched));
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1219
|
+
await this.deleteProduct(event.data.object.id);
|
|
1220
|
+
} else {
|
|
1221
|
+
throw err;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
case "product.deleted": {
|
|
1227
|
+
const product = event.data.object;
|
|
1228
|
+
this.config.logger?.info(
|
|
1229
|
+
`Received webhook ${event.id}: ${event.type} for product ${product.id}`
|
|
1230
|
+
);
|
|
1231
|
+
await this.deleteProduct(product.id);
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
case "price.created":
|
|
1235
|
+
case "price.updated": {
|
|
1236
|
+
try {
|
|
1237
|
+
const { entity: price, refetched } = await this.fetchOrUseWebhookData(
|
|
1238
|
+
event.data.object,
|
|
1239
|
+
(id) => this.stripe.prices.retrieve(id)
|
|
1240
|
+
);
|
|
1241
|
+
this.config.logger?.info(
|
|
1242
|
+
`Received webhook ${event.id}: ${event.type} for price ${price.id}`
|
|
1243
|
+
);
|
|
1244
|
+
await this.upsertPrices([price], false, this.getSyncTimestamp(event, refetched));
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1247
|
+
await this.deletePrice(event.data.object.id);
|
|
1248
|
+
} else {
|
|
1249
|
+
throw err;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
case "price.deleted": {
|
|
1255
|
+
const price = event.data.object;
|
|
1256
|
+
this.config.logger?.info(
|
|
1257
|
+
`Received webhook ${event.id}: ${event.type} for price ${price.id}`
|
|
1258
|
+
);
|
|
1259
|
+
await this.deletePrice(price.id);
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
case "plan.created":
|
|
1263
|
+
case "plan.updated": {
|
|
1264
|
+
try {
|
|
1265
|
+
const { entity: plan, refetched } = await this.fetchOrUseWebhookData(
|
|
1266
|
+
event.data.object,
|
|
1267
|
+
(id) => this.stripe.plans.retrieve(id)
|
|
1268
|
+
);
|
|
1269
|
+
this.config.logger?.info(
|
|
1270
|
+
`Received webhook ${event.id}: ${event.type} for plan ${plan.id}`
|
|
1271
|
+
);
|
|
1272
|
+
await this.upsertPlans([plan], false, this.getSyncTimestamp(event, refetched));
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
if (err instanceof import_stripe.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1275
|
+
await this.deletePlan(event.data.object.id);
|
|
1276
|
+
} else {
|
|
1277
|
+
throw err;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case "plan.deleted": {
|
|
1283
|
+
const plan = event.data.object;
|
|
1284
|
+
this.config.logger?.info(`Received webhook ${event.id}: ${event.type} for plan ${plan.id}`);
|
|
1285
|
+
await this.deletePlan(plan.id);
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
case "setup_intent.canceled":
|
|
1289
|
+
case "setup_intent.created":
|
|
1290
|
+
case "setup_intent.requires_action":
|
|
1291
|
+
case "setup_intent.setup_failed":
|
|
1292
|
+
case "setup_intent.succeeded": {
|
|
1293
|
+
const { entity: setupIntent, refetched } = await this.fetchOrUseWebhookData(
|
|
1294
|
+
event.data.object,
|
|
1295
|
+
(id) => this.stripe.setupIntents.retrieve(id),
|
|
1296
|
+
(setupIntent2) => setupIntent2.status === "canceled" || setupIntent2.status === "succeeded"
|
|
1297
|
+
);
|
|
1298
|
+
this.config.logger?.info(
|
|
1299
|
+
`Received webhook ${event.id}: ${event.type} for setupIntent ${setupIntent.id}`
|
|
1300
|
+
);
|
|
1301
|
+
await this.upsertSetupIntents([setupIntent], false, this.getSyncTimestamp(event, refetched));
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case "subscription_schedule.aborted":
|
|
1305
|
+
case "subscription_schedule.canceled":
|
|
1306
|
+
case "subscription_schedule.completed":
|
|
1307
|
+
case "subscription_schedule.created":
|
|
1308
|
+
case "subscription_schedule.expiring":
|
|
1309
|
+
case "subscription_schedule.released":
|
|
1310
|
+
case "subscription_schedule.updated": {
|
|
1311
|
+
const { entity: subscriptionSchedule, refetched } = await this.fetchOrUseWebhookData(
|
|
1312
|
+
event.data.object,
|
|
1313
|
+
(id) => this.stripe.subscriptionSchedules.retrieve(id),
|
|
1314
|
+
(schedule) => schedule.status === "canceled" || schedule.status === "completed"
|
|
1315
|
+
);
|
|
1316
|
+
this.config.logger?.info(
|
|
1317
|
+
`Received webhook ${event.id}: ${event.type} for subscriptionSchedule ${subscriptionSchedule.id}`
|
|
1318
|
+
);
|
|
1319
|
+
await this.upsertSubscriptionSchedules(
|
|
1320
|
+
[subscriptionSchedule],
|
|
1321
|
+
false,
|
|
1322
|
+
this.getSyncTimestamp(event, refetched)
|
|
1323
|
+
);
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
case "payment_method.attached":
|
|
1327
|
+
case "payment_method.automatically_updated":
|
|
1328
|
+
case "payment_method.detached":
|
|
1329
|
+
case "payment_method.updated": {
|
|
1330
|
+
const { entity: paymentMethod, refetched } = await this.fetchOrUseWebhookData(
|
|
1331
|
+
event.data.object,
|
|
1332
|
+
(id) => this.stripe.paymentMethods.retrieve(id)
|
|
1333
|
+
);
|
|
1334
|
+
this.config.logger?.info(
|
|
1335
|
+
`Received webhook ${event.id}: ${event.type} for paymentMethod ${paymentMethod.id}`
|
|
1336
|
+
);
|
|
1337
|
+
await this.upsertPaymentMethods(
|
|
1338
|
+
[paymentMethod],
|
|
1339
|
+
false,
|
|
1340
|
+
this.getSyncTimestamp(event, refetched)
|
|
1341
|
+
);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
case "charge.dispute.created":
|
|
1345
|
+
case "charge.dispute.funds_reinstated":
|
|
1346
|
+
case "charge.dispute.funds_withdrawn":
|
|
1347
|
+
case "charge.dispute.updated":
|
|
1348
|
+
case "charge.dispute.closed": {
|
|
1349
|
+
const { entity: dispute, refetched } = await this.fetchOrUseWebhookData(
|
|
1350
|
+
event.data.object,
|
|
1351
|
+
(id) => this.stripe.disputes.retrieve(id),
|
|
1352
|
+
(dispute2) => dispute2.status === "won" || dispute2.status === "lost"
|
|
1353
|
+
);
|
|
1354
|
+
this.config.logger?.info(
|
|
1355
|
+
`Received webhook ${event.id}: ${event.type} for dispute ${dispute.id}`
|
|
1356
|
+
);
|
|
1357
|
+
await this.upsertDisputes([dispute], false, this.getSyncTimestamp(event, refetched));
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
case "payment_intent.amount_capturable_updated":
|
|
1361
|
+
case "payment_intent.canceled":
|
|
1362
|
+
case "payment_intent.created":
|
|
1363
|
+
case "payment_intent.partially_funded":
|
|
1364
|
+
case "payment_intent.payment_failed":
|
|
1365
|
+
case "payment_intent.processing":
|
|
1366
|
+
case "payment_intent.requires_action":
|
|
1367
|
+
case "payment_intent.succeeded": {
|
|
1368
|
+
const { entity: paymentIntent, refetched } = await this.fetchOrUseWebhookData(
|
|
1369
|
+
event.data.object,
|
|
1370
|
+
(id) => this.stripe.paymentIntents.retrieve(id),
|
|
1371
|
+
// Final states - do not re-fetch from API
|
|
1372
|
+
(entity) => entity.status === "canceled" || entity.status === "succeeded"
|
|
1373
|
+
);
|
|
1374
|
+
this.config.logger?.info(
|
|
1375
|
+
`Received webhook ${event.id}: ${event.type} for paymentIntent ${paymentIntent.id}`
|
|
1376
|
+
);
|
|
1377
|
+
await this.upsertPaymentIntents(
|
|
1378
|
+
[paymentIntent],
|
|
1379
|
+
false,
|
|
1380
|
+
this.getSyncTimestamp(event, refetched)
|
|
1381
|
+
);
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
case "credit_note.created":
|
|
1385
|
+
case "credit_note.updated":
|
|
1386
|
+
case "credit_note.voided": {
|
|
1387
|
+
const { entity: creditNote, refetched } = await this.fetchOrUseWebhookData(
|
|
1388
|
+
event.data.object,
|
|
1389
|
+
(id) => this.stripe.creditNotes.retrieve(id),
|
|
1390
|
+
(creditNote2) => creditNote2.status === "void"
|
|
1391
|
+
);
|
|
1392
|
+
this.config.logger?.info(
|
|
1393
|
+
`Received webhook ${event.id}: ${event.type} for creditNote ${creditNote.id}`
|
|
1394
|
+
);
|
|
1395
|
+
await this.upsertCreditNotes([creditNote], false, this.getSyncTimestamp(event, refetched));
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
case "radar.early_fraud_warning.created":
|
|
1399
|
+
case "radar.early_fraud_warning.updated": {
|
|
1400
|
+
const { entity: earlyFraudWarning, refetched } = await this.fetchOrUseWebhookData(
|
|
1401
|
+
event.data.object,
|
|
1402
|
+
(id) => this.stripe.radar.earlyFraudWarnings.retrieve(id)
|
|
1403
|
+
);
|
|
1404
|
+
this.config.logger?.info(
|
|
1405
|
+
`Received webhook ${event.id}: ${event.type} for earlyFraudWarning ${earlyFraudWarning.id}`
|
|
1406
|
+
);
|
|
1407
|
+
await this.upsertEarlyFraudWarning(
|
|
1408
|
+
[earlyFraudWarning],
|
|
1409
|
+
false,
|
|
1410
|
+
this.getSyncTimestamp(event, refetched)
|
|
1411
|
+
);
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
case "refund.created":
|
|
1415
|
+
case "refund.failed":
|
|
1416
|
+
case "refund.updated":
|
|
1417
|
+
case "charge.refund.updated": {
|
|
1418
|
+
const { entity: refund, refetched } = await this.fetchOrUseWebhookData(
|
|
1419
|
+
event.data.object,
|
|
1420
|
+
(id) => this.stripe.refunds.retrieve(id)
|
|
1421
|
+
);
|
|
1422
|
+
this.config.logger?.info(
|
|
1423
|
+
`Received webhook ${event.id}: ${event.type} for refund ${refund.id}`
|
|
1424
|
+
);
|
|
1425
|
+
await this.upsertRefunds([refund], false, this.getSyncTimestamp(event, refetched));
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
case "review.closed":
|
|
1429
|
+
case "review.opened": {
|
|
1430
|
+
const { entity: review, refetched } = await this.fetchOrUseWebhookData(
|
|
1431
|
+
event.data.object,
|
|
1432
|
+
(id) => this.stripe.reviews.retrieve(id)
|
|
1433
|
+
);
|
|
1434
|
+
this.config.logger?.info(
|
|
1435
|
+
`Received webhook ${event.id}: ${event.type} for review ${review.id}`
|
|
1436
|
+
);
|
|
1437
|
+
await this.upsertReviews([review], false, this.getSyncTimestamp(event, refetched));
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
case "entitlements.active_entitlement_summary.updated": {
|
|
1441
|
+
const activeEntitlementSummary = event.data.object;
|
|
1442
|
+
let entitlements = activeEntitlementSummary.entitlements;
|
|
1443
|
+
let refetched = false;
|
|
1444
|
+
if (this.config.revalidateObjectsViaStripeApi?.includes("entitlements")) {
|
|
1445
|
+
const { lastResponse, ...rest } = await this.stripe.entitlements.activeEntitlements.list({
|
|
1446
|
+
customer: activeEntitlementSummary.customer
|
|
1447
|
+
});
|
|
1448
|
+
entitlements = rest;
|
|
1449
|
+
refetched = true;
|
|
1450
|
+
}
|
|
1451
|
+
this.config.logger?.info(
|
|
1452
|
+
`Received webhook ${event.id}: ${event.type} for activeEntitlementSummary for customer ${activeEntitlementSummary.customer}`
|
|
1453
|
+
);
|
|
1454
|
+
await this.deleteRemovedActiveEntitlements(
|
|
1455
|
+
activeEntitlementSummary.customer,
|
|
1456
|
+
entitlements.data.map((entitlement) => entitlement.id)
|
|
1457
|
+
);
|
|
1458
|
+
await this.upsertActiveEntitlements(
|
|
1459
|
+
activeEntitlementSummary.customer,
|
|
1460
|
+
entitlements.data,
|
|
1461
|
+
false,
|
|
1462
|
+
this.getSyncTimestamp(event, refetched)
|
|
1463
|
+
);
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
default:
|
|
1467
|
+
throw new Error("Unhandled webhook event");
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
getSyncTimestamp(event, refetched) {
|
|
1471
|
+
return refetched ? (/* @__PURE__ */ new Date()).toISOString() : new Date(event.created * 1e3).toISOString();
|
|
1472
|
+
}
|
|
1473
|
+
shouldRefetchEntity(entity) {
|
|
1474
|
+
return this.config.revalidateObjectsViaStripeApi?.includes(entity.object);
|
|
1475
|
+
}
|
|
1476
|
+
async fetchOrUseWebhookData(entity, fetchFn, entityInFinalState) {
|
|
1477
|
+
if (!entity.id) return { entity, refetched: false };
|
|
1478
|
+
if (entityInFinalState && entityInFinalState(entity)) return { entity, refetched: false };
|
|
1479
|
+
if (this.shouldRefetchEntity(entity)) {
|
|
1480
|
+
const fetchedEntity = await fetchFn(entity.id);
|
|
1481
|
+
return { entity: fetchedEntity, refetched: true };
|
|
1482
|
+
}
|
|
1483
|
+
return { entity, refetched: false };
|
|
1484
|
+
}
|
|
1485
|
+
async syncSingleEntity(stripeId) {
|
|
1486
|
+
if (stripeId.startsWith("cus_")) {
|
|
1487
|
+
return this.stripe.customers.retrieve(stripeId).then((it) => {
|
|
1488
|
+
if (!it || it.deleted) return;
|
|
1489
|
+
return this.upsertCustomers([it]);
|
|
1490
|
+
});
|
|
1491
|
+
} else if (stripeId.startsWith("in_")) {
|
|
1492
|
+
return this.stripe.invoices.retrieve(stripeId).then((it) => this.upsertInvoices([it]));
|
|
1493
|
+
} else if (stripeId.startsWith("price_")) {
|
|
1494
|
+
return this.stripe.prices.retrieve(stripeId).then((it) => this.upsertPrices([it]));
|
|
1495
|
+
} else if (stripeId.startsWith("prod_")) {
|
|
1496
|
+
return this.stripe.products.retrieve(stripeId).then((it) => this.upsertProducts([it]));
|
|
1497
|
+
} else if (stripeId.startsWith("sub_")) {
|
|
1498
|
+
return this.stripe.subscriptions.retrieve(stripeId).then((it) => this.upsertSubscriptions([it]));
|
|
1499
|
+
} else if (stripeId.startsWith("seti_")) {
|
|
1500
|
+
return this.stripe.setupIntents.retrieve(stripeId).then((it) => this.upsertSetupIntents([it]));
|
|
1501
|
+
} else if (stripeId.startsWith("pm_")) {
|
|
1502
|
+
return this.stripe.paymentMethods.retrieve(stripeId).then((it) => this.upsertPaymentMethods([it]));
|
|
1503
|
+
} else if (stripeId.startsWith("dp_") || stripeId.startsWith("du_")) {
|
|
1504
|
+
return this.stripe.disputes.retrieve(stripeId).then((it) => this.upsertDisputes([it]));
|
|
1505
|
+
} else if (stripeId.startsWith("ch_")) {
|
|
1506
|
+
return this.stripe.charges.retrieve(stripeId).then((it) => this.upsertCharges([it], true));
|
|
1507
|
+
} else if (stripeId.startsWith("pi_")) {
|
|
1508
|
+
return this.stripe.paymentIntents.retrieve(stripeId).then((it) => this.upsertPaymentIntents([it]));
|
|
1509
|
+
} else if (stripeId.startsWith("txi_")) {
|
|
1510
|
+
return this.stripe.taxIds.retrieve(stripeId).then((it) => this.upsertTaxIds([it]));
|
|
1511
|
+
} else if (stripeId.startsWith("cn_")) {
|
|
1512
|
+
return this.stripe.creditNotes.retrieve(stripeId).then((it) => this.upsertCreditNotes([it]));
|
|
1513
|
+
} else if (stripeId.startsWith("issfr_")) {
|
|
1514
|
+
return this.stripe.radar.earlyFraudWarnings.retrieve(stripeId).then((it) => this.upsertEarlyFraudWarning([it]));
|
|
1515
|
+
} else if (stripeId.startsWith("prv_")) {
|
|
1516
|
+
return this.stripe.reviews.retrieve(stripeId).then((it) => this.upsertReviews([it]));
|
|
1517
|
+
} else if (stripeId.startsWith("re_")) {
|
|
1518
|
+
return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it]));
|
|
1519
|
+
} else if (stripeId.startsWith("feat_")) {
|
|
1520
|
+
return this.stripe.entitlements.features.retrieve(stripeId).then((it) => this.upsertFeatures([it]));
|
|
1521
|
+
} else if (stripeId.startsWith("cs_")) {
|
|
1522
|
+
return this.stripe.checkout.sessions.retrieve(stripeId).then((it) => this.upsertCheckoutSessions([it]));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async syncBackfill(params) {
|
|
1526
|
+
const { object } = params ?? {};
|
|
1527
|
+
let products, prices, customers, checkoutSessions, subscriptions, subscriptionSchedules, invoices, setupIntents, paymentMethods, disputes, charges, paymentIntents, plans, taxIds, creditNotes, earlyFraudWarnings, refunds;
|
|
1528
|
+
switch (object) {
|
|
1529
|
+
case "all":
|
|
1530
|
+
products = await this.syncProducts(params);
|
|
1531
|
+
prices = await this.syncPrices(params);
|
|
1532
|
+
plans = await this.syncPlans(params);
|
|
1533
|
+
customers = await this.syncCustomers(params);
|
|
1534
|
+
subscriptions = await this.syncSubscriptions(params);
|
|
1535
|
+
subscriptionSchedules = await this.syncSubscriptionSchedules(params);
|
|
1536
|
+
invoices = await this.syncInvoices(params);
|
|
1537
|
+
charges = await this.syncCharges(params);
|
|
1538
|
+
setupIntents = await this.syncSetupIntents(params);
|
|
1539
|
+
paymentMethods = await this.syncPaymentMethods(params);
|
|
1540
|
+
paymentIntents = await this.syncPaymentIntents(params);
|
|
1541
|
+
taxIds = await this.syncTaxIds(params);
|
|
1542
|
+
creditNotes = await this.syncCreditNotes(params);
|
|
1543
|
+
disputes = await this.syncDisputes(params);
|
|
1544
|
+
earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
|
|
1545
|
+
refunds = await this.syncRefunds(params);
|
|
1546
|
+
checkoutSessions = await this.syncCheckoutSessions(params);
|
|
1547
|
+
break;
|
|
1548
|
+
case "customer":
|
|
1549
|
+
customers = await this.syncCustomers(params);
|
|
1550
|
+
break;
|
|
1551
|
+
case "invoice":
|
|
1552
|
+
invoices = await this.syncInvoices(params);
|
|
1553
|
+
break;
|
|
1554
|
+
case "price":
|
|
1555
|
+
prices = await this.syncPrices(params);
|
|
1556
|
+
break;
|
|
1557
|
+
case "product":
|
|
1558
|
+
products = await this.syncProducts(params);
|
|
1559
|
+
break;
|
|
1560
|
+
case "subscription":
|
|
1561
|
+
subscriptions = await this.syncSubscriptions(params);
|
|
1562
|
+
break;
|
|
1563
|
+
case "subscription_schedules":
|
|
1564
|
+
subscriptionSchedules = await this.syncSubscriptionSchedules(params);
|
|
1565
|
+
break;
|
|
1566
|
+
case "setup_intent":
|
|
1567
|
+
setupIntents = await this.syncSetupIntents(params);
|
|
1568
|
+
break;
|
|
1569
|
+
case "payment_method":
|
|
1570
|
+
paymentMethods = await this.syncPaymentMethods(params);
|
|
1571
|
+
break;
|
|
1572
|
+
case "dispute":
|
|
1573
|
+
disputes = await this.syncDisputes(params);
|
|
1574
|
+
break;
|
|
1575
|
+
case "charge":
|
|
1576
|
+
charges = await this.syncCharges(params);
|
|
1577
|
+
break;
|
|
1578
|
+
case "payment_intent":
|
|
1579
|
+
paymentIntents = await this.syncPaymentIntents(params);
|
|
1580
|
+
case "plan":
|
|
1581
|
+
plans = await this.syncPlans(params);
|
|
1582
|
+
break;
|
|
1583
|
+
case "tax_id":
|
|
1584
|
+
taxIds = await this.syncTaxIds(params);
|
|
1585
|
+
break;
|
|
1586
|
+
case "credit_note":
|
|
1587
|
+
creditNotes = await this.syncCreditNotes(params);
|
|
1588
|
+
break;
|
|
1589
|
+
case "early_fraud_warning":
|
|
1590
|
+
earlyFraudWarnings = await this.syncEarlyFraudWarnings(params);
|
|
1591
|
+
break;
|
|
1592
|
+
case "refund":
|
|
1593
|
+
refunds = await this.syncRefunds(params);
|
|
1594
|
+
break;
|
|
1595
|
+
case "checkout_sessions":
|
|
1596
|
+
checkoutSessions = await this.syncCheckoutSessions(params);
|
|
1597
|
+
break;
|
|
1598
|
+
default:
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
products,
|
|
1603
|
+
prices,
|
|
1604
|
+
customers,
|
|
1605
|
+
checkoutSessions,
|
|
1606
|
+
subscriptions,
|
|
1607
|
+
subscriptionSchedules,
|
|
1608
|
+
invoices,
|
|
1609
|
+
setupIntents,
|
|
1610
|
+
paymentMethods,
|
|
1611
|
+
disputes,
|
|
1612
|
+
charges,
|
|
1613
|
+
paymentIntents,
|
|
1614
|
+
plans,
|
|
1615
|
+
taxIds,
|
|
1616
|
+
creditNotes,
|
|
1617
|
+
earlyFraudWarnings,
|
|
1618
|
+
refunds
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
async syncProducts(syncParams) {
|
|
1622
|
+
this.config.logger?.info("Syncing products");
|
|
1623
|
+
const params = { limit: 100 };
|
|
1624
|
+
if (syncParams?.created) params.created = syncParams?.created;
|
|
1625
|
+
return this.fetchAndUpsert(
|
|
1626
|
+
() => this.stripe.products.list(params),
|
|
1627
|
+
(products) => this.upsertProducts(products)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
async syncPrices(syncParams) {
|
|
1631
|
+
this.config.logger?.info("Syncing prices");
|
|
1632
|
+
const params = { limit: 100 };
|
|
1633
|
+
if (syncParams?.created) params.created = syncParams?.created;
|
|
1634
|
+
return this.fetchAndUpsert(
|
|
1635
|
+
() => this.stripe.prices.list(params),
|
|
1636
|
+
(prices) => this.upsertPrices(prices, syncParams?.backfillRelatedEntities)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
async syncPlans(syncParams) {
|
|
1640
|
+
this.config.logger?.info("Syncing plans");
|
|
1641
|
+
const params = { limit: 100 };
|
|
1642
|
+
if (syncParams?.created) params.created = syncParams?.created;
|
|
1643
|
+
return this.fetchAndUpsert(
|
|
1644
|
+
() => this.stripe.plans.list(params),
|
|
1645
|
+
(plans) => this.upsertPlans(plans, syncParams?.backfillRelatedEntities)
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
async syncCustomers(syncParams) {
|
|
1649
|
+
this.config.logger?.info("Syncing customers");
|
|
1650
|
+
const params = { limit: 100 };
|
|
1651
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1652
|
+
return this.fetchAndUpsert(
|
|
1653
|
+
() => this.stripe.customers.list(params),
|
|
1654
|
+
// @ts-expect-error
|
|
1655
|
+
(items) => this.upsertCustomers(items)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
async syncSubscriptions(syncParams) {
|
|
1659
|
+
this.config.logger?.info("Syncing subscriptions");
|
|
1660
|
+
const params = { status: "all", limit: 100 };
|
|
1661
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1662
|
+
return this.fetchAndUpsert(
|
|
1663
|
+
() => this.stripe.subscriptions.list(params),
|
|
1664
|
+
(items) => this.upsertSubscriptions(items, syncParams?.backfillRelatedEntities)
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
async syncSubscriptionSchedules(syncParams) {
|
|
1668
|
+
this.config.logger?.info("Syncing subscription schedules");
|
|
1669
|
+
const params = { limit: 100 };
|
|
1670
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1671
|
+
return this.fetchAndUpsert(
|
|
1672
|
+
() => this.stripe.subscriptionSchedules.list(params),
|
|
1673
|
+
(items) => this.upsertSubscriptionSchedules(items, syncParams?.backfillRelatedEntities)
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
async syncInvoices(syncParams) {
|
|
1677
|
+
this.config.logger?.info("Syncing invoices");
|
|
1678
|
+
const params = { limit: 100 };
|
|
1679
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1680
|
+
return this.fetchAndUpsert(
|
|
1681
|
+
() => this.stripe.invoices.list(params),
|
|
1682
|
+
(items) => this.upsertInvoices(items, syncParams?.backfillRelatedEntities)
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
async syncCharges(syncParams) {
|
|
1686
|
+
this.config.logger?.info("Syncing charges");
|
|
1687
|
+
const params = { limit: 100 };
|
|
1688
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1689
|
+
return this.fetchAndUpsert(
|
|
1690
|
+
() => this.stripe.charges.list(params),
|
|
1691
|
+
(items) => this.upsertCharges(items, syncParams?.backfillRelatedEntities)
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
async syncSetupIntents(syncParams) {
|
|
1695
|
+
this.config.logger?.info("Syncing setup_intents");
|
|
1696
|
+
const params = { limit: 100 };
|
|
1697
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1698
|
+
return this.fetchAndUpsert(
|
|
1699
|
+
() => this.stripe.setupIntents.list(params),
|
|
1700
|
+
(items) => this.upsertSetupIntents(items, syncParams?.backfillRelatedEntities)
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
async syncPaymentIntents(syncParams) {
|
|
1704
|
+
this.config.logger?.info("Syncing payment_intents");
|
|
1705
|
+
const params = { limit: 100 };
|
|
1706
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1707
|
+
return this.fetchAndUpsert(
|
|
1708
|
+
() => this.stripe.paymentIntents.list(params),
|
|
1709
|
+
(items) => this.upsertPaymentIntents(items, syncParams?.backfillRelatedEntities)
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
async syncTaxIds(syncParams) {
|
|
1713
|
+
this.config.logger?.info("Syncing tax_ids");
|
|
1714
|
+
const params = { limit: 100 };
|
|
1715
|
+
return this.fetchAndUpsert(
|
|
1716
|
+
() => this.stripe.taxIds.list(params),
|
|
1717
|
+
(items) => this.upsertTaxIds(items, syncParams?.backfillRelatedEntities)
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
async syncPaymentMethods(syncParams) {
|
|
1721
|
+
this.config.logger?.info("Syncing payment method");
|
|
1722
|
+
const prepared = (0, import_yesql2.pg)(
|
|
1723
|
+
`select id from "${this.config.schema}"."customers" WHERE deleted <> true;`
|
|
1724
|
+
)([]);
|
|
1725
|
+
const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
|
|
1726
|
+
this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
|
|
1727
|
+
let synced = 0;
|
|
1728
|
+
for (const customerIdChunk of chunkArray(customerIds, 10)) {
|
|
1729
|
+
await Promise.all(
|
|
1730
|
+
customerIdChunk.map(async (customerId) => {
|
|
1731
|
+
const syncResult = await this.fetchAndUpsert(
|
|
1732
|
+
() => this.stripe.paymentMethods.list({
|
|
1733
|
+
limit: 100,
|
|
1734
|
+
customer: customerId
|
|
1735
|
+
}),
|
|
1736
|
+
(items) => this.upsertPaymentMethods(items, syncParams?.backfillRelatedEntities)
|
|
1737
|
+
);
|
|
1738
|
+
synced += syncResult.synced;
|
|
1739
|
+
})
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
return { synced };
|
|
1743
|
+
}
|
|
1744
|
+
async syncDisputes(syncParams) {
|
|
1745
|
+
const params = { limit: 100 };
|
|
1746
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1747
|
+
return this.fetchAndUpsert(
|
|
1748
|
+
() => this.stripe.disputes.list(params),
|
|
1749
|
+
(items) => this.upsertDisputes(items, syncParams?.backfillRelatedEntities)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
async syncEarlyFraudWarnings(syncParams) {
|
|
1753
|
+
this.config.logger?.info("Syncing early fraud warnings");
|
|
1754
|
+
const params = { limit: 100 };
|
|
1755
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1756
|
+
return this.fetchAndUpsert(
|
|
1757
|
+
() => this.stripe.radar.earlyFraudWarnings.list(params),
|
|
1758
|
+
(items) => this.upsertEarlyFraudWarning(items, syncParams?.backfillRelatedEntities)
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
async syncRefunds(syncParams) {
|
|
1762
|
+
this.config.logger?.info("Syncing refunds");
|
|
1763
|
+
const params = { limit: 100 };
|
|
1764
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1765
|
+
return this.fetchAndUpsert(
|
|
1766
|
+
() => this.stripe.refunds.list(params),
|
|
1767
|
+
(items) => this.upsertRefunds(items, syncParams?.backfillRelatedEntities)
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
async syncCreditNotes(syncParams) {
|
|
1771
|
+
this.config.logger?.info("Syncing credit notes");
|
|
1772
|
+
const params = { limit: 100 };
|
|
1773
|
+
if (syncParams?.created) params.created = syncParams?.created;
|
|
1774
|
+
return this.fetchAndUpsert(
|
|
1775
|
+
() => this.stripe.creditNotes.list(params),
|
|
1776
|
+
(creditNotes) => this.upsertCreditNotes(creditNotes)
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
async syncFeatures(syncParams) {
|
|
1780
|
+
this.config.logger?.info("Syncing features");
|
|
1781
|
+
const params = { limit: 100, ...syncParams?.pagination };
|
|
1782
|
+
return this.fetchAndUpsert(
|
|
1783
|
+
() => this.stripe.entitlements.features.list(params),
|
|
1784
|
+
(features) => this.upsertFeatures(features)
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
async syncEntitlements(customerId, syncParams) {
|
|
1788
|
+
this.config.logger?.info("Syncing entitlements");
|
|
1789
|
+
const params = {
|
|
1790
|
+
customer: customerId,
|
|
1791
|
+
limit: 100,
|
|
1792
|
+
...syncParams?.pagination
|
|
1793
|
+
};
|
|
1794
|
+
return this.fetchAndUpsert(
|
|
1795
|
+
() => this.stripe.entitlements.activeEntitlements.list(params),
|
|
1796
|
+
(entitlements) => this.upsertActiveEntitlements(customerId, entitlements)
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
async syncCheckoutSessions(syncParams) {
|
|
1800
|
+
this.config.logger?.info("Syncing checkout sessions");
|
|
1801
|
+
const params = {
|
|
1802
|
+
limit: 100
|
|
1803
|
+
};
|
|
1804
|
+
if (syncParams?.created) params.created = syncParams.created;
|
|
1805
|
+
return this.fetchAndUpsert(
|
|
1806
|
+
() => this.stripe.checkout.sessions.list(params),
|
|
1807
|
+
(items) => this.upsertCheckoutSessions(items, syncParams?.backfillRelatedEntities)
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
async fetchAndUpsert(fetch, upsert) {
|
|
1811
|
+
const items = [];
|
|
1812
|
+
this.config.logger?.info("Fetching items to sync from Stripe");
|
|
1813
|
+
for await (const item of fetch()) {
|
|
1814
|
+
items.push(item);
|
|
1815
|
+
}
|
|
1816
|
+
if (!items.length) return { synced: 0 };
|
|
1817
|
+
this.config.logger?.info(`Upserting ${items.length} items`);
|
|
1818
|
+
const chunkSize = 250;
|
|
1819
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
1820
|
+
const chunk = items.slice(i, i + chunkSize);
|
|
1821
|
+
await upsert(chunk);
|
|
1822
|
+
}
|
|
1823
|
+
this.config.logger?.info("Upserted items");
|
|
1824
|
+
return { synced: items.length };
|
|
1825
|
+
}
|
|
1826
|
+
async upsertCharges(charges, backfillRelatedEntities, syncTimestamp) {
|
|
1827
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1828
|
+
await Promise.all([
|
|
1829
|
+
this.backfillCustomers(getUniqueIds(charges, "customer")),
|
|
1830
|
+
this.backfillInvoices(getUniqueIds(charges, "invoice"))
|
|
1831
|
+
]);
|
|
1832
|
+
}
|
|
1833
|
+
await this.expandEntity(
|
|
1834
|
+
charges,
|
|
1835
|
+
"refunds",
|
|
1836
|
+
(id) => this.stripe.refunds.list({ charge: id, limit: 100 })
|
|
1837
|
+
);
|
|
1838
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1839
|
+
charges,
|
|
1840
|
+
"charges",
|
|
1841
|
+
chargeSchema,
|
|
1842
|
+
syncTimestamp
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
async backfillCharges(chargeIds) {
|
|
1846
|
+
const missingChargeIds = await this.postgresClient.findMissingEntries("charges", chargeIds);
|
|
1847
|
+
await this.fetchMissingEntities(
|
|
1848
|
+
missingChargeIds,
|
|
1849
|
+
(id) => this.stripe.charges.retrieve(id)
|
|
1850
|
+
).then((charges) => this.upsertCharges(charges));
|
|
1851
|
+
}
|
|
1852
|
+
async backfillPaymentIntents(paymentIntentIds) {
|
|
1853
|
+
const missingIds = await this.postgresClient.findMissingEntries(
|
|
1854
|
+
"payment_intents",
|
|
1855
|
+
paymentIntentIds
|
|
1856
|
+
);
|
|
1857
|
+
await this.fetchMissingEntities(
|
|
1858
|
+
missingIds,
|
|
1859
|
+
(id) => this.stripe.paymentIntents.retrieve(id)
|
|
1860
|
+
).then((paymentIntents) => this.upsertPaymentIntents(paymentIntents));
|
|
1861
|
+
}
|
|
1862
|
+
async upsertCreditNotes(creditNotes, backfillRelatedEntities, syncTimestamp) {
|
|
1863
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1864
|
+
await Promise.all([
|
|
1865
|
+
this.backfillCustomers(getUniqueIds(creditNotes, "customer")),
|
|
1866
|
+
this.backfillInvoices(getUniqueIds(creditNotes, "invoice"))
|
|
1867
|
+
]);
|
|
1868
|
+
}
|
|
1869
|
+
await this.expandEntity(
|
|
1870
|
+
creditNotes,
|
|
1871
|
+
"lines",
|
|
1872
|
+
(id) => this.stripe.creditNotes.listLineItems(id, { limit: 100 })
|
|
1873
|
+
);
|
|
1874
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1875
|
+
creditNotes,
|
|
1876
|
+
"credit_notes",
|
|
1877
|
+
creditNoteSchema,
|
|
1878
|
+
syncTimestamp
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
async upsertCheckoutSessions(checkoutSessions, backfillRelatedEntities, syncTimestamp) {
|
|
1882
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1883
|
+
await Promise.all([
|
|
1884
|
+
this.backfillCustomers(getUniqueIds(checkoutSessions, "customer")),
|
|
1885
|
+
this.backfillSubscriptions(getUniqueIds(checkoutSessions, "subscription")),
|
|
1886
|
+
this.backfillPaymentIntents(getUniqueIds(checkoutSessions, "payment_intent")),
|
|
1887
|
+
this.backfillInvoices(getUniqueIds(checkoutSessions, "invoice"))
|
|
1888
|
+
]);
|
|
1889
|
+
}
|
|
1890
|
+
const rows = await this.postgresClient.upsertManyWithTimestampProtection(
|
|
1891
|
+
checkoutSessions,
|
|
1892
|
+
"checkout_sessions",
|
|
1893
|
+
checkoutSessionSchema,
|
|
1894
|
+
syncTimestamp
|
|
1895
|
+
);
|
|
1896
|
+
await this.fillCheckoutSessionsLineItems(
|
|
1897
|
+
checkoutSessions.map((cs) => cs.id),
|
|
1898
|
+
syncTimestamp
|
|
1899
|
+
);
|
|
1900
|
+
return rows;
|
|
1901
|
+
}
|
|
1902
|
+
async upsertEarlyFraudWarning(earlyFraudWarnings, backfillRelatedEntities, syncTimestamp) {
|
|
1903
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1904
|
+
await Promise.all([
|
|
1905
|
+
this.backfillPaymentIntents(getUniqueIds(earlyFraudWarnings, "payment_intent")),
|
|
1906
|
+
this.backfillCharges(getUniqueIds(earlyFraudWarnings, "charge"))
|
|
1907
|
+
]);
|
|
1908
|
+
}
|
|
1909
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1910
|
+
earlyFraudWarnings,
|
|
1911
|
+
"early_fraud_warnings",
|
|
1912
|
+
earlyFraudWarningSchema,
|
|
1913
|
+
syncTimestamp
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
async upsertRefunds(refunds, backfillRelatedEntities, syncTimestamp) {
|
|
1917
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1918
|
+
await Promise.all([
|
|
1919
|
+
this.backfillPaymentIntents(getUniqueIds(refunds, "payment_intent")),
|
|
1920
|
+
this.backfillCharges(getUniqueIds(refunds, "charge"))
|
|
1921
|
+
]);
|
|
1922
|
+
}
|
|
1923
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1924
|
+
refunds,
|
|
1925
|
+
"refunds",
|
|
1926
|
+
refundSchema,
|
|
1927
|
+
syncTimestamp
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
async upsertReviews(reviews, backfillRelatedEntities, syncTimestamp) {
|
|
1931
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1932
|
+
await Promise.all([
|
|
1933
|
+
this.backfillPaymentIntents(getUniqueIds(reviews, "payment_intent")),
|
|
1934
|
+
this.backfillCharges(getUniqueIds(reviews, "charge"))
|
|
1935
|
+
]);
|
|
1936
|
+
}
|
|
1937
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1938
|
+
reviews,
|
|
1939
|
+
"reviews",
|
|
1940
|
+
reviewSchema,
|
|
1941
|
+
syncTimestamp
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
async upsertCustomers(customers, syncTimestamp) {
|
|
1945
|
+
const deletedCustomers = customers.filter((customer) => customer.deleted);
|
|
1946
|
+
const nonDeletedCustomers = customers.filter((customer) => !customer.deleted);
|
|
1947
|
+
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
1948
|
+
nonDeletedCustomers,
|
|
1949
|
+
"customers",
|
|
1950
|
+
customerSchema,
|
|
1951
|
+
syncTimestamp
|
|
1952
|
+
);
|
|
1953
|
+
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
1954
|
+
deletedCustomers,
|
|
1955
|
+
"customers",
|
|
1956
|
+
customerDeletedSchema,
|
|
1957
|
+
syncTimestamp
|
|
1958
|
+
);
|
|
1959
|
+
return customers;
|
|
1960
|
+
}
|
|
1961
|
+
async backfillCustomers(customerIds) {
|
|
1962
|
+
const missingIds = await this.postgresClient.findMissingEntries("customers", customerIds);
|
|
1963
|
+
await this.fetchMissingEntities(missingIds, (id) => this.stripe.customers.retrieve(id)).then((entries) => this.upsertCustomers(entries)).catch((err) => {
|
|
1964
|
+
this.config.logger?.error(err, "Failed to backfill");
|
|
1965
|
+
throw err;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
async upsertDisputes(disputes, backfillRelatedEntities, syncTimestamp) {
|
|
1969
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1970
|
+
await this.backfillCharges(getUniqueIds(disputes, "charge"));
|
|
1971
|
+
}
|
|
1972
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1973
|
+
disputes,
|
|
1974
|
+
"disputes",
|
|
1975
|
+
disputeSchema,
|
|
1976
|
+
syncTimestamp
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
async upsertInvoices(invoices, backfillRelatedEntities, syncTimestamp) {
|
|
1980
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
1981
|
+
await Promise.all([
|
|
1982
|
+
this.backfillCustomers(getUniqueIds(invoices, "customer")),
|
|
1983
|
+
this.backfillSubscriptions(getUniqueIds(invoices, "subscription"))
|
|
1984
|
+
]);
|
|
1985
|
+
}
|
|
1986
|
+
await this.expandEntity(
|
|
1987
|
+
invoices,
|
|
1988
|
+
"lines",
|
|
1989
|
+
(id) => this.stripe.invoices.listLineItems(id, { limit: 100 })
|
|
1990
|
+
);
|
|
1991
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
1992
|
+
invoices,
|
|
1993
|
+
"invoices",
|
|
1994
|
+
invoiceSchema,
|
|
1995
|
+
syncTimestamp
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
backfillInvoices = async (invoiceIds) => {
|
|
1999
|
+
const missingIds = await this.postgresClient.findMissingEntries("invoices", invoiceIds);
|
|
2000
|
+
await this.fetchMissingEntities(missingIds, (id) => this.stripe.invoices.retrieve(id)).then(
|
|
2001
|
+
(entries) => this.upsertInvoices(entries)
|
|
2002
|
+
);
|
|
2003
|
+
};
|
|
2004
|
+
backfillPrices = async (priceIds) => {
|
|
2005
|
+
const missingIds = await this.postgresClient.findMissingEntries("prices", priceIds);
|
|
2006
|
+
await this.fetchMissingEntities(missingIds, (id) => this.stripe.prices.retrieve(id)).then(
|
|
2007
|
+
(entries) => this.upsertPrices(entries)
|
|
2008
|
+
);
|
|
2009
|
+
};
|
|
2010
|
+
async upsertPlans(plans, backfillRelatedEntities, syncTimestamp) {
|
|
2011
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2012
|
+
await this.backfillProducts(getUniqueIds(plans, "product"));
|
|
2013
|
+
}
|
|
2014
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2015
|
+
plans,
|
|
2016
|
+
"plans",
|
|
2017
|
+
planSchema,
|
|
2018
|
+
syncTimestamp
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
async deletePlan(id) {
|
|
2022
|
+
return this.postgresClient.delete("plans", id);
|
|
2023
|
+
}
|
|
2024
|
+
async upsertPrices(prices, backfillRelatedEntities, syncTimestamp) {
|
|
2025
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2026
|
+
await this.backfillProducts(getUniqueIds(prices, "product"));
|
|
2027
|
+
}
|
|
2028
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2029
|
+
prices,
|
|
2030
|
+
"prices",
|
|
2031
|
+
priceSchema,
|
|
2032
|
+
syncTimestamp
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
async deletePrice(id) {
|
|
2036
|
+
return this.postgresClient.delete("prices", id);
|
|
2037
|
+
}
|
|
2038
|
+
async upsertProducts(products, syncTimestamp) {
|
|
2039
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2040
|
+
products,
|
|
2041
|
+
"products",
|
|
2042
|
+
productSchema,
|
|
2043
|
+
syncTimestamp
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
async deleteProduct(id) {
|
|
2047
|
+
return this.postgresClient.delete("products", id);
|
|
2048
|
+
}
|
|
2049
|
+
async backfillProducts(productIds) {
|
|
2050
|
+
const missingProductIds = await this.postgresClient.findMissingEntries("products", productIds);
|
|
2051
|
+
await this.fetchMissingEntities(
|
|
2052
|
+
missingProductIds,
|
|
2053
|
+
(id) => this.stripe.products.retrieve(id)
|
|
2054
|
+
).then((products) => this.upsertProducts(products));
|
|
2055
|
+
}
|
|
2056
|
+
async upsertPaymentIntents(paymentIntents, backfillRelatedEntities, syncTimestamp) {
|
|
2057
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2058
|
+
await Promise.all([
|
|
2059
|
+
this.backfillCustomers(getUniqueIds(paymentIntents, "customer")),
|
|
2060
|
+
this.backfillInvoices(getUniqueIds(paymentIntents, "invoice"))
|
|
2061
|
+
]);
|
|
2062
|
+
}
|
|
2063
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2064
|
+
paymentIntents,
|
|
2065
|
+
"payment_intents",
|
|
2066
|
+
paymentIntentSchema,
|
|
2067
|
+
syncTimestamp
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
async upsertPaymentMethods(paymentMethods, backfillRelatedEntities = false, syncTimestamp) {
|
|
2071
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2072
|
+
await this.backfillCustomers(getUniqueIds(paymentMethods, "customer"));
|
|
2073
|
+
}
|
|
2074
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2075
|
+
paymentMethods,
|
|
2076
|
+
"payment_methods",
|
|
2077
|
+
paymentMethodsSchema,
|
|
2078
|
+
syncTimestamp
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
async upsertSetupIntents(setupIntents, backfillRelatedEntities, syncTimestamp) {
|
|
2082
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2083
|
+
await this.backfillCustomers(getUniqueIds(setupIntents, "customer"));
|
|
2084
|
+
}
|
|
2085
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2086
|
+
setupIntents,
|
|
2087
|
+
"setup_intents",
|
|
2088
|
+
setupIntentsSchema,
|
|
2089
|
+
syncTimestamp
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
async upsertTaxIds(taxIds, backfillRelatedEntities, syncTimestamp) {
|
|
2093
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2094
|
+
await this.backfillCustomers(getUniqueIds(taxIds, "customer"));
|
|
2095
|
+
}
|
|
2096
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2097
|
+
taxIds,
|
|
2098
|
+
"tax_ids",
|
|
2099
|
+
taxIdSchema,
|
|
2100
|
+
syncTimestamp
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
async deleteTaxId(id) {
|
|
2104
|
+
return this.postgresClient.delete("tax_ids", id);
|
|
2105
|
+
}
|
|
2106
|
+
async upsertSubscriptionItems(subscriptionItems, syncTimestamp) {
|
|
2107
|
+
const modifiedSubscriptionItems = subscriptionItems.map((subscriptionItem) => {
|
|
2108
|
+
const priceId = subscriptionItem.price.id.toString();
|
|
2109
|
+
const deleted = subscriptionItem.deleted;
|
|
2110
|
+
const quantity = subscriptionItem.quantity;
|
|
2111
|
+
return {
|
|
2112
|
+
...subscriptionItem,
|
|
2113
|
+
price: priceId,
|
|
2114
|
+
deleted: deleted ?? false,
|
|
2115
|
+
quantity: quantity ?? null
|
|
2116
|
+
};
|
|
2117
|
+
});
|
|
2118
|
+
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2119
|
+
modifiedSubscriptionItems,
|
|
2120
|
+
"subscription_items",
|
|
2121
|
+
subscriptionItemSchema,
|
|
2122
|
+
syncTimestamp
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
async fillCheckoutSessionsLineItems(checkoutSessionIds, syncTimestamp) {
|
|
2126
|
+
for (const checkoutSessionId of checkoutSessionIds) {
|
|
2127
|
+
const lineItemResponses = [];
|
|
2128
|
+
for await (const lineItem of this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
|
|
2129
|
+
limit: 100
|
|
2130
|
+
})) {
|
|
2131
|
+
lineItemResponses.push(lineItem);
|
|
2132
|
+
}
|
|
2133
|
+
await this.upsertCheckoutSessionLineItems(lineItemResponses, checkoutSessionId, syncTimestamp);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async upsertCheckoutSessionLineItems(lineItems, checkoutSessionId, syncTimestamp) {
|
|
2137
|
+
await this.backfillPrices(
|
|
2138
|
+
lineItems.map((lineItem) => lineItem.price?.id?.toString() ?? void 0).filter((id) => id !== void 0)
|
|
2139
|
+
);
|
|
2140
|
+
const modifiedLineItems = lineItems.map((lineItem) => {
|
|
2141
|
+
const priceId = typeof lineItem.price === "object" && lineItem.price?.id ? lineItem.price.id.toString() : lineItem.price?.toString() || null;
|
|
2142
|
+
return {
|
|
2143
|
+
...lineItem,
|
|
2144
|
+
price: priceId,
|
|
2145
|
+
checkout_session: checkoutSessionId
|
|
2146
|
+
};
|
|
2147
|
+
});
|
|
2148
|
+
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2149
|
+
modifiedLineItems,
|
|
2150
|
+
"checkout_session_line_items",
|
|
2151
|
+
checkoutSessionLineItemSchema,
|
|
2152
|
+
syncTimestamp
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
async markDeletedSubscriptionItems(subscriptionId, currentSubItemIds) {
|
|
2156
|
+
let prepared = (0, import_yesql2.pg)(`
|
|
2157
|
+
select id from "${this.config.schema}"."subscription_items"
|
|
2158
|
+
where subscription = :subscriptionId and deleted = false;
|
|
2159
|
+
`)({ subscriptionId });
|
|
2160
|
+
const { rows } = await this.postgresClient.query(prepared.text, prepared.values);
|
|
2161
|
+
const deletedIds = rows.filter(
|
|
2162
|
+
({ id }) => currentSubItemIds.includes(id) === false
|
|
2163
|
+
);
|
|
2164
|
+
if (deletedIds.length > 0) {
|
|
2165
|
+
const ids = deletedIds.map(({ id }) => id);
|
|
2166
|
+
prepared = (0, import_yesql2.pg)(`
|
|
2167
|
+
update "${this.config.schema}"."subscription_items"
|
|
2168
|
+
set deleted = true where id=any(:ids::text[]);
|
|
2169
|
+
`)({ ids });
|
|
2170
|
+
const { rowCount } = await await this.postgresClient.query(prepared.text, prepared.values);
|
|
2171
|
+
return { rowCount: rowCount || 0 };
|
|
2172
|
+
} else {
|
|
2173
|
+
return { rowCount: 0 };
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
async upsertSubscriptionSchedules(subscriptionSchedules, backfillRelatedEntities, syncTimestamp) {
|
|
2177
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2178
|
+
const customerIds = getUniqueIds(subscriptionSchedules, "customer");
|
|
2179
|
+
await this.backfillCustomers(customerIds);
|
|
2180
|
+
}
|
|
2181
|
+
const rows = await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2182
|
+
subscriptionSchedules,
|
|
2183
|
+
"subscription_schedules",
|
|
2184
|
+
subscriptionScheduleSchema,
|
|
2185
|
+
syncTimestamp
|
|
2186
|
+
);
|
|
2187
|
+
return rows;
|
|
2188
|
+
}
|
|
2189
|
+
async upsertSubscriptions(subscriptions, backfillRelatedEntities, syncTimestamp) {
|
|
2190
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2191
|
+
const customerIds = getUniqueIds(subscriptions, "customer");
|
|
2192
|
+
await this.backfillCustomers(customerIds);
|
|
2193
|
+
}
|
|
2194
|
+
await this.expandEntity(
|
|
2195
|
+
subscriptions,
|
|
2196
|
+
"items",
|
|
2197
|
+
(id) => this.stripe.subscriptionItems.list({ subscription: id, limit: 100 })
|
|
2198
|
+
);
|
|
2199
|
+
const rows = await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2200
|
+
subscriptions,
|
|
2201
|
+
"subscriptions",
|
|
2202
|
+
subscriptionSchema,
|
|
2203
|
+
syncTimestamp
|
|
2204
|
+
);
|
|
2205
|
+
const allSubscriptionItems = subscriptions.flatMap((subscription) => subscription.items.data);
|
|
2206
|
+
await this.upsertSubscriptionItems(allSubscriptionItems, syncTimestamp);
|
|
2207
|
+
const markSubscriptionItemsDeleted = [];
|
|
2208
|
+
for (const subscription of subscriptions) {
|
|
2209
|
+
const subscriptionItems = subscription.items.data;
|
|
2210
|
+
const subItemIds = subscriptionItems.map((x) => x.id);
|
|
2211
|
+
markSubscriptionItemsDeleted.push(
|
|
2212
|
+
this.markDeletedSubscriptionItems(subscription.id, subItemIds)
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
await Promise.all(markSubscriptionItemsDeleted);
|
|
2216
|
+
return rows;
|
|
2217
|
+
}
|
|
2218
|
+
async deleteRemovedActiveEntitlements(customerId, currentActiveEntitlementIds) {
|
|
2219
|
+
const prepared = (0, import_yesql2.pg)(`
|
|
2220
|
+
delete from "${this.config.schema}"."active_entitlements"
|
|
2221
|
+
where customer = :customerId and id <> ALL(:currentActiveEntitlementIds::text[]);
|
|
2222
|
+
`)({ customerId, currentActiveEntitlementIds });
|
|
2223
|
+
const { rowCount } = await this.postgresClient.query(prepared.text, prepared.values);
|
|
2224
|
+
return { rowCount: rowCount || 0 };
|
|
2225
|
+
}
|
|
2226
|
+
async upsertFeatures(features, syncTimestamp) {
|
|
2227
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2228
|
+
features,
|
|
2229
|
+
"features",
|
|
2230
|
+
featureSchema,
|
|
2231
|
+
syncTimestamp
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
async backfillFeatures(featureIds) {
|
|
2235
|
+
const missingFeatureIds = await this.postgresClient.findMissingEntries("features", featureIds);
|
|
2236
|
+
await this.fetchMissingEntities(
|
|
2237
|
+
missingFeatureIds,
|
|
2238
|
+
(id) => this.stripe.entitlements.features.retrieve(id)
|
|
2239
|
+
).then((features) => this.upsertFeatures(features)).catch((err) => {
|
|
2240
|
+
this.config.logger?.error(err, "Failed to backfill features");
|
|
2241
|
+
throw err;
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
async upsertActiveEntitlements(customerId, activeEntitlements, backfillRelatedEntities, syncTimestamp) {
|
|
2245
|
+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
|
|
2246
|
+
await Promise.all([
|
|
2247
|
+
this.backfillCustomers(getUniqueIds(activeEntitlements, "customer")),
|
|
2248
|
+
this.backfillFeatures(getUniqueIds(activeEntitlements, "feature"))
|
|
2249
|
+
]);
|
|
2250
|
+
}
|
|
2251
|
+
const entitlements = activeEntitlements.map((entitlement) => ({
|
|
2252
|
+
id: entitlement.id,
|
|
2253
|
+
object: entitlement.object,
|
|
2254
|
+
feature: typeof entitlement.feature === "string" ? entitlement.feature : entitlement.feature.id,
|
|
2255
|
+
customer: customerId,
|
|
2256
|
+
livemode: entitlement.livemode,
|
|
2257
|
+
lookup_key: entitlement.lookup_key
|
|
2258
|
+
}));
|
|
2259
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2260
|
+
entitlements,
|
|
2261
|
+
"active_entitlements",
|
|
2262
|
+
activeEntitlementSchema,
|
|
2263
|
+
syncTimestamp
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
// Managed Webhook CRUD methods
|
|
2267
|
+
async createManagedWebhook(baseUrl, params) {
|
|
2268
|
+
const uuid = (0, import_node_crypto.randomUUID)();
|
|
2269
|
+
const webhookUrl = `${baseUrl}/${uuid}`;
|
|
2270
|
+
const webhook = await this.stripe.webhookEndpoints.create({
|
|
2271
|
+
...params,
|
|
2272
|
+
url: webhookUrl
|
|
2273
|
+
});
|
|
2274
|
+
const webhookWithUuid = { ...webhook, uuid };
|
|
2275
|
+
await this.upsertManagedWebhooks([webhookWithUuid]);
|
|
2276
|
+
return { webhook, uuid };
|
|
2277
|
+
}
|
|
2278
|
+
async getManagedWebhook(id) {
|
|
2279
|
+
const result = await this.postgresClient.query(
|
|
2280
|
+
`SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" WHERE id = $1`,
|
|
2281
|
+
[id]
|
|
2282
|
+
);
|
|
2283
|
+
return result.rows.length > 0 ? result.rows[0] : null;
|
|
2284
|
+
}
|
|
2285
|
+
async listManagedWebhooks() {
|
|
2286
|
+
const result = await this.postgresClient.query(
|
|
2287
|
+
`SELECT * FROM "${this.config.schema || DEFAULT_SCHEMA}"."managed_webhooks" ORDER BY created DESC`
|
|
2288
|
+
);
|
|
2289
|
+
return result.rows;
|
|
2290
|
+
}
|
|
2291
|
+
async updateManagedWebhook(id, params) {
|
|
2292
|
+
const webhook = await this.stripe.webhookEndpoints.update(id, params);
|
|
2293
|
+
const existing = await this.getManagedWebhook(id);
|
|
2294
|
+
const webhookWithUuid = { ...webhook, uuid: existing?.uuid || (0, import_node_crypto.randomUUID)() };
|
|
2295
|
+
await this.upsertManagedWebhooks([webhookWithUuid]);
|
|
2296
|
+
return webhook;
|
|
2297
|
+
}
|
|
2298
|
+
async deleteManagedWebhook(id) {
|
|
2299
|
+
await this.stripe.webhookEndpoints.del(id);
|
|
2300
|
+
return this.postgresClient.delete("managed_webhooks", id);
|
|
2301
|
+
}
|
|
2302
|
+
async upsertManagedWebhooks(webhooks, syncTimestamp) {
|
|
2303
|
+
return this.postgresClient.upsertManyWithTimestampProtection(
|
|
2304
|
+
webhooks,
|
|
2305
|
+
"managed_webhooks",
|
|
2306
|
+
managedWebhookSchema,
|
|
2307
|
+
syncTimestamp
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
async backfillSubscriptions(subscriptionIds) {
|
|
2311
|
+
const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
|
|
2312
|
+
"subscriptions",
|
|
2313
|
+
subscriptionIds
|
|
2314
|
+
);
|
|
2315
|
+
await this.fetchMissingEntities(
|
|
2316
|
+
missingSubscriptionIds,
|
|
2317
|
+
(id) => this.stripe.subscriptions.retrieve(id)
|
|
2318
|
+
).then((subscriptions) => this.upsertSubscriptions(subscriptions));
|
|
2319
|
+
}
|
|
2320
|
+
backfillSubscriptionSchedules = async (subscriptionIds) => {
|
|
2321
|
+
const missingSubscriptionIds = await this.postgresClient.findMissingEntries(
|
|
2322
|
+
"subscription_schedules",
|
|
2323
|
+
subscriptionIds
|
|
2324
|
+
);
|
|
2325
|
+
await this.fetchMissingEntities(
|
|
2326
|
+
missingSubscriptionIds,
|
|
2327
|
+
(id) => this.stripe.subscriptionSchedules.retrieve(id)
|
|
2328
|
+
).then((subscriptionSchedules) => this.upsertSubscriptionSchedules(subscriptionSchedules));
|
|
2329
|
+
};
|
|
2330
|
+
/**
|
|
2331
|
+
* Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
|
|
2332
|
+
*/
|
|
2333
|
+
async expandEntity(entities, property, listFn) {
|
|
2334
|
+
if (!this.config.autoExpandLists) return;
|
|
2335
|
+
for (const entity of entities) {
|
|
2336
|
+
if (entity[property]?.has_more) {
|
|
2337
|
+
const allData = [];
|
|
2338
|
+
for await (const fetchedEntity of listFn(entity.id)) {
|
|
2339
|
+
allData.push(fetchedEntity);
|
|
2340
|
+
}
|
|
2341
|
+
entity[property] = {
|
|
2342
|
+
...entity[property],
|
|
2343
|
+
data: allData,
|
|
2344
|
+
has_more: false
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async fetchMissingEntities(ids, fetch) {
|
|
2350
|
+
if (!ids.length) return [];
|
|
2351
|
+
const entities = [];
|
|
2352
|
+
for (const id of ids) {
|
|
2353
|
+
const entity = await fetch(id);
|
|
2354
|
+
entities.push(entity);
|
|
2355
|
+
}
|
|
2356
|
+
return entities;
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
function chunkArray(array, chunkSize) {
|
|
2360
|
+
const result = [];
|
|
2361
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
2362
|
+
result.push(array.slice(i, i + chunkSize));
|
|
2363
|
+
}
|
|
2364
|
+
return result;
|
|
2365
|
+
}
|
|
2366
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2367
|
+
0 && (module.exports = {
|
|
2368
|
+
PostgresClient,
|
|
2369
|
+
StripeAutoSync
|
|
2370
|
+
});
|