payweave 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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -0
  3. package/dist/_tsup-dts-rollup.d.ts +19470 -0
  4. package/dist/chunk-4R5OGOCC.js +94 -0
  5. package/dist/chunk-4R5OGOCC.js.map +1 -0
  6. package/dist/chunk-535Q3GUN.js +175 -0
  7. package/dist/chunk-535Q3GUN.js.map +1 -0
  8. package/dist/chunk-67NRYGRC.js +192 -0
  9. package/dist/chunk-67NRYGRC.js.map +1 -0
  10. package/dist/chunk-CGQ73I4I.js +568 -0
  11. package/dist/chunk-CGQ73I4I.js.map +1 -0
  12. package/dist/chunk-CJEYDHY6.js +18 -0
  13. package/dist/chunk-CJEYDHY6.js.map +1 -0
  14. package/dist/chunk-CR63JAAS.js +3 -0
  15. package/dist/chunk-CR63JAAS.js.map +1 -0
  16. package/dist/chunk-E633MKVX.js +1507 -0
  17. package/dist/chunk-E633MKVX.js.map +1 -0
  18. package/dist/chunk-FBPOCXRZ.js +1135 -0
  19. package/dist/chunk-FBPOCXRZ.js.map +1 -0
  20. package/dist/chunk-HTRGMZFH.js +267 -0
  21. package/dist/chunk-HTRGMZFH.js.map +1 -0
  22. package/dist/chunk-IUVGM3K7.js +765 -0
  23. package/dist/chunk-IUVGM3K7.js.map +1 -0
  24. package/dist/chunk-JD4OZHRI.js +326 -0
  25. package/dist/chunk-JD4OZHRI.js.map +1 -0
  26. package/dist/chunk-NV3MZF7L.js +139 -0
  27. package/dist/chunk-NV3MZF7L.js.map +1 -0
  28. package/dist/chunk-NXQBP35U.js +13 -0
  29. package/dist/chunk-NXQBP35U.js.map +1 -0
  30. package/dist/chunk-QJHXDZYC.js +412 -0
  31. package/dist/chunk-QJHXDZYC.js.map +1 -0
  32. package/dist/chunk-TWZK62TZ.js +83 -0
  33. package/dist/chunk-TWZK62TZ.js.map +1 -0
  34. package/dist/cli/index.js +35381 -0
  35. package/dist/core/index.d.ts +85 -0
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/db/drizzle/index.d.ts +6 -0
  39. package/dist/db/drizzle/index.js +1532 -0
  40. package/dist/db/drizzle/index.js.map +1 -0
  41. package/dist/db/index.d.ts +43 -0
  42. package/dist/db/index.js +3 -0
  43. package/dist/db/index.js.map +1 -0
  44. package/dist/db/mongodb/index.d.ts +3 -0
  45. package/dist/db/mongodb/index.js +934 -0
  46. package/dist/db/mongodb/index.js.map +1 -0
  47. package/dist/db/mysql/index.d.ts +1 -0
  48. package/dist/db/mysql/index.js +12 -0
  49. package/dist/db/mysql/index.js.map +1 -0
  50. package/dist/db/postgres/index.d.ts +5 -0
  51. package/dist/db/postgres/index.js +791 -0
  52. package/dist/db/postgres/index.js.map +1 -0
  53. package/dist/db/prisma/index.d.ts +1 -0
  54. package/dist/db/prisma/index.js +12 -0
  55. package/dist/db/prisma/index.js.map +1 -0
  56. package/dist/db/sqlite/index.d.ts +5 -0
  57. package/dist/db/sqlite/index.js +750 -0
  58. package/dist/db/sqlite/index.js.map +1 -0
  59. package/dist/express/index.d.ts +1 -0
  60. package/dist/express/index.js +3 -0
  61. package/dist/express/index.js.map +1 -0
  62. package/dist/fastify/index.d.ts +1 -0
  63. package/dist/fastify/index.js +3 -0
  64. package/dist/fastify/index.js.map +1 -0
  65. package/dist/flutterwave/index.d.ts +27 -0
  66. package/dist/flutterwave/index.js +9 -0
  67. package/dist/flutterwave/index.js.map +1 -0
  68. package/dist/index.d.ts +85 -0
  69. package/dist/index.js +3677 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/next/index.d.ts +1 -0
  72. package/dist/next/index.js +3 -0
  73. package/dist/next/index.js.map +1 -0
  74. package/dist/paystack/index.d.ts +33 -0
  75. package/dist/paystack/index.js +9 -0
  76. package/dist/paystack/index.js.map +1 -0
  77. package/dist/products/index.d.ts +22 -0
  78. package/dist/products/index.js +7 -0
  79. package/dist/products/index.js.map +1 -0
  80. package/dist/testing/index.d.ts +11 -0
  81. package/dist/testing/index.js +69 -0
  82. package/dist/testing/index.js.map +1 -0
  83. package/dist/unified/index.d.ts +41 -0
  84. package/dist/unified/index.js +6 -0
  85. package/dist/unified/index.js.map +1 -0
  86. package/dist/webhooks/index.d.ts +8 -0
  87. package/dist/webhooks/index.js +8 -0
  88. package/dist/webhooks/index.js.map +1 -0
  89. package/package.json +164 -0
@@ -0,0 +1,934 @@
1
+ import { PW_TABLES, PW_ACTIVE_SUBSCRIPTION_STATUSES, DEFAULT_STALE_CLAIM_AFTER_MS } from '../../chunk-NV3MZF7L.js';
2
+ import { currentPeriod, DAY_MS, WEEK_MS } from '../../chunk-4R5OGOCC.js';
3
+ import { PayweaveConfigError, PayweaveNotFoundError, PayweaveError, PayweaveValidationError } from '../../chunk-HTRGMZFH.js';
4
+ import { randomInt } from 'crypto';
5
+
6
+ // src/db/mongodb/errors.ts
7
+ function installHintError(cause) {
8
+ return new PayweaveConfigError(
9
+ 'payweave/db/mongodb: connecting requires the "mongodb" package, which is not installed (it is an optional peerDependency of "payweave"). Run `npm install mongodb` (or your package manager\'s equivalent) and try again.',
10
+ { cause }
11
+ );
12
+ }
13
+ var DUPLICATE_KEY_CODE = 11e3;
14
+ function isDuplicateKeyError(error) {
15
+ const code = error?.code;
16
+ return code === DUPLICATE_KEY_CODE;
17
+ }
18
+ function wrapDriverError(error, message) {
19
+ if (error instanceof PayweaveError) return error;
20
+ if (isDuplicateKeyError(error)) {
21
+ return new PayweaveValidationError(message, { cause: error });
22
+ }
23
+ return new PayweaveError(message, { cause: error, isRetryable: false });
24
+ }
25
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
26
+ var ENCODING_LEN = ENCODING.length;
27
+ var TIME_LEN = 10;
28
+ var RANDOM_LEN = 16;
29
+ function encodeTime(timeMs, len) {
30
+ if (!Number.isSafeInteger(timeMs) || timeMs < 0) {
31
+ throw new RangeError(`ulid: time must be a non-negative safe integer \u2014 got ${timeMs}`);
32
+ }
33
+ let remaining = timeMs;
34
+ let out = "";
35
+ for (let i = 0; i < len; i++) {
36
+ const mod = remaining % ENCODING_LEN;
37
+ out = ENCODING.charAt(mod) + out;
38
+ remaining = Math.floor(remaining / ENCODING_LEN);
39
+ }
40
+ return out;
41
+ }
42
+ function encodeRandom(len) {
43
+ let out = "";
44
+ for (let i = 0; i < len; i++) {
45
+ out += ENCODING.charAt(randomInt(0, ENCODING_LEN));
46
+ }
47
+ return out;
48
+ }
49
+ function ulid(timeMs = Date.now()) {
50
+ return encodeTime(timeMs, TIME_LEN) + encodeRandom(RANDOM_LEN);
51
+ }
52
+ function generatePwId(timeMs = Date.now()) {
53
+ return `pwv_${ulid(timeMs)}`;
54
+ }
55
+
56
+ // src/db/mongodb/migrations.ts
57
+ var MIGRATION_NAME = "0001_init";
58
+ var REQUIRED_INDEXES = [
59
+ {
60
+ collection: PW_TABLES.customers,
61
+ name: "pw_customers_externalId_unique",
62
+ key: { externalId: 1 },
63
+ options: { unique: true }
64
+ },
65
+ {
66
+ collection: PW_TABLES.plans,
67
+ name: "pw_plans_planId_version_unique",
68
+ key: { planId: 1, version: 1 },
69
+ options: { unique: true }
70
+ },
71
+ {
72
+ collection: PW_TABLES.subscriptions,
73
+ name: "pw_subscriptions_active_unique",
74
+ key: { customerId: 1, group: 1 },
75
+ options: {
76
+ unique: true,
77
+ // The §2 partial-unique active-subscription rule: at most one row per
78
+ // (customerId, group) whose status is in the active set — everything
79
+ // else (canceled/incomplete, and any OTHER status a community fork
80
+ // might add) falls outside the filter and never collides.
81
+ partialFilterExpression: { status: { $in: [...PW_ACTIVE_SUBSCRIPTION_STATUSES] } }
82
+ }
83
+ },
84
+ {
85
+ collection: PW_TABLES.featureBalances,
86
+ name: "pw_feature_balances_unique",
87
+ key: { customerId: 1, featureId: 1, group: 1 },
88
+ options: { unique: true }
89
+ }
90
+ ];
91
+ function isNamespaceNotFound(error) {
92
+ const code = error?.code;
93
+ return code === 26;
94
+ }
95
+ async function existingIndexNames(db, collectionName) {
96
+ try {
97
+ const infos = await db.collection(collectionName).indexes();
98
+ return new Set(infos.map((i) => i.name).filter((n) => typeof n === "string"));
99
+ } catch (error) {
100
+ if (isNamespaceNotFound(error)) return /* @__PURE__ */ new Set();
101
+ throw error;
102
+ }
103
+ }
104
+ async function missingIndexes(db) {
105
+ const missing = [];
106
+ const byCollection = /* @__PURE__ */ new Map();
107
+ for (const required of REQUIRED_INDEXES) {
108
+ let names = byCollection.get(required.collection);
109
+ if (!names) {
110
+ names = await existingIndexNames(db, required.collection);
111
+ byCollection.set(required.collection, names);
112
+ }
113
+ if (!names.has(required.name)) missing.push(required);
114
+ }
115
+ return missing;
116
+ }
117
+ async function mongoMigrationStatus(db) {
118
+ const missing = await missingIndexes(db);
119
+ return missing.length === 0 ? { pending: [], applied: [MIGRATION_NAME] } : { pending: [MIGRATION_NAME], applied: [] };
120
+ }
121
+ async function mongoMigrationApply(db) {
122
+ const missing = await missingIndexes(db);
123
+ if (missing.length === 0) return { applied: [] };
124
+ for (const required of missing) {
125
+ await db.collection(required.collection).createIndex(required.key, {
126
+ name: required.name,
127
+ ...required.options
128
+ });
129
+ }
130
+ return { applied: [MIGRATION_NAME] };
131
+ }
132
+
133
+ // src/db/mongodb/period-pipeline.ts
134
+ function buildPeriodExpr(nowMs, anchorExpr, resetExpr) {
135
+ const nowLiteral = new Date(nowMs);
136
+ return {
137
+ $let: {
138
+ vars: { anchor: anchorExpr, reset: resetExpr },
139
+ in: {
140
+ $switch: {
141
+ branches: [
142
+ {
143
+ case: { $in: ["$$reset", ["day", "week"]] },
144
+ then: {
145
+ $let: {
146
+ vars: { periodMs: { $cond: [{ $eq: ["$$reset", "day"] }, DAY_MS, WEEK_MS] } },
147
+ in: {
148
+ $let: {
149
+ vars: {
150
+ idx: {
151
+ $max: [
152
+ 0,
153
+ {
154
+ $floor: {
155
+ $divide: [{ $subtract: [nowLiteral, "$$anchor"] }, "$$periodMs"]
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ },
161
+ in: {
162
+ index: "$$idx",
163
+ start: { $add: ["$$anchor", { $multiply: ["$$idx", "$$periodMs"] }] },
164
+ end: {
165
+ $add: ["$$anchor", { $multiply: [{ $add: ["$$idx", 1] }, "$$periodMs"] }]
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ ],
174
+ default: {
175
+ $let: {
176
+ vars: {
177
+ monthsPerPeriod: { $cond: [{ $eq: ["$$reset", "year"] }, 12, 1] },
178
+ anchorParts: { $dateToParts: { date: "$$anchor", timezone: "UTC" } },
179
+ nowParts: { $dateToParts: { date: nowLiteral, timezone: "UTC" } }
180
+ },
181
+ in: {
182
+ $let: {
183
+ vars: {
184
+ monthDiff: {
185
+ $add: [
186
+ { $multiply: [{ $subtract: ["$$nowParts.year", "$$anchorParts.year"] }, 12] },
187
+ { $subtract: ["$$nowParts.month", "$$anchorParts.month"] }
188
+ ]
189
+ }
190
+ },
191
+ in: {
192
+ $let: {
193
+ vars: {
194
+ idx0: {
195
+ $max: [
196
+ 0,
197
+ {
198
+ $subtract: [
199
+ { $floor: { $divide: ["$$monthDiff", "$$monthsPerPeriod"] } },
200
+ 1
201
+ ]
202
+ }
203
+ ]
204
+ }
205
+ },
206
+ in: {
207
+ $let: {
208
+ vars: {
209
+ end0: {
210
+ $dateAdd: {
211
+ startDate: "$$anchor",
212
+ unit: "month",
213
+ amount: { $multiply: [{ $add: ["$$idx0", 1] }, "$$monthsPerPeriod"] }
214
+ }
215
+ },
216
+ end1: {
217
+ $dateAdd: {
218
+ startDate: "$$anchor",
219
+ unit: "month",
220
+ amount: { $multiply: [{ $add: ["$$idx0", 2] }, "$$monthsPerPeriod"] }
221
+ }
222
+ },
223
+ end2: {
224
+ $dateAdd: {
225
+ startDate: "$$anchor",
226
+ unit: "month",
227
+ amount: { $multiply: [{ $add: ["$$idx0", 3] }, "$$monthsPerPeriod"] }
228
+ }
229
+ },
230
+ end3: {
231
+ $dateAdd: {
232
+ startDate: "$$anchor",
233
+ unit: "month",
234
+ amount: { $multiply: [{ $add: ["$$idx0", 4] }, "$$monthsPerPeriod"] }
235
+ }
236
+ }
237
+ },
238
+ in: {
239
+ $let: {
240
+ vars: {
241
+ chosenIdx: {
242
+ $switch: {
243
+ branches: [
244
+ { case: { $lt: [nowLiteral, "$$end0"] }, then: "$$idx0" },
245
+ {
246
+ case: { $lt: [nowLiteral, "$$end1"] },
247
+ then: { $add: ["$$idx0", 1] }
248
+ },
249
+ {
250
+ case: { $lt: [nowLiteral, "$$end2"] },
251
+ then: { $add: ["$$idx0", 2] }
252
+ }
253
+ ],
254
+ default: { $add: ["$$idx0", 3] }
255
+ }
256
+ },
257
+ chosenEnd: {
258
+ $switch: {
259
+ branches: [
260
+ { case: { $lt: [nowLiteral, "$$end0"] }, then: "$$end0" },
261
+ { case: { $lt: [nowLiteral, "$$end1"] }, then: "$$end1" },
262
+ { case: { $lt: [nowLiteral, "$$end2"] }, then: "$$end2" }
263
+ ],
264
+ default: "$$end3"
265
+ }
266
+ }
267
+ },
268
+ in: {
269
+ index: "$$chosenIdx",
270
+ start: {
271
+ $dateAdd: {
272
+ startDate: "$$anchor",
273
+ unit: "month",
274
+ amount: { $multiply: ["$$chosenIdx", "$$monthsPerPeriod"] }
275
+ }
276
+ },
277
+ end: "$$chosenEnd"
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ };
293
+ }
294
+
295
+ // src/db/mongodb/rows.ts
296
+ function docToCustomer(doc) {
297
+ return {
298
+ id: doc._id,
299
+ externalId: doc.externalId,
300
+ providerIds: doc.providerIds ?? {},
301
+ email: doc.email ?? null,
302
+ createdAt: doc.createdAt,
303
+ updatedAt: doc.updatedAt
304
+ };
305
+ }
306
+ function docToPlanVersion(doc) {
307
+ return {
308
+ id: doc._id,
309
+ planId: doc.planId,
310
+ version: doc.version,
311
+ group: doc.group,
312
+ isDefault: doc.isDefault,
313
+ name: doc.name,
314
+ priceMinor: doc.priceMinor,
315
+ priceCurrency: doc.priceCurrency,
316
+ priceInterval: doc.priceInterval,
317
+ features: doc.features,
318
+ providerRefs: doc.providerRefs,
319
+ pushedAt: doc.pushedAt
320
+ };
321
+ }
322
+ function docToSubscription(doc) {
323
+ return {
324
+ id: doc._id,
325
+ customerId: doc.customerId,
326
+ planId: doc.planId,
327
+ planVersion: doc.planVersion,
328
+ group: doc.group,
329
+ status: doc.status,
330
+ provider: doc.provider,
331
+ providerSubscriptionRef: doc.providerSubscriptionRef,
332
+ currentPeriodStart: doc.currentPeriodStart,
333
+ currentPeriodEnd: doc.currentPeriodEnd,
334
+ cancelAtPeriodEnd: doc.cancelAtPeriodEnd,
335
+ createdAt: doc.createdAt,
336
+ updatedAt: doc.updatedAt
337
+ };
338
+ }
339
+ function docToFeatureBalance(doc) {
340
+ return {
341
+ id: doc._id,
342
+ customerId: doc.customerId,
343
+ featureId: doc.featureId,
344
+ group: doc.group,
345
+ used: doc.used,
346
+ limit: doc.limit,
347
+ resetInterval: doc.resetInterval,
348
+ anchor: doc.anchor,
349
+ periodStart: doc.periodStart,
350
+ periodEnd: doc.periodEnd,
351
+ planId: doc.planId,
352
+ planVersion: doc.planVersion,
353
+ updatedAt: doc.updatedAt
354
+ };
355
+ }
356
+
357
+ // src/db/mongodb/topology.ts
358
+ var UNSUPPORTED_MESSAGE_PATTERN = /transaction numbers are only allowed on a replica set member or mongos|this mongodb deployment does not support transactions|transactions are not supported|standalone/i;
359
+ function isTransactionsUnsupportedError(error) {
360
+ if (error === null || typeof error !== "object") return false;
361
+ const record = error;
362
+ const message = typeof record.message === "string" ? record.message : "";
363
+ if (message.length === 0) return false;
364
+ if (record.code === 20 && /transaction/i.test(message)) return true;
365
+ return /transaction/i.test(message) && UNSUPPORTED_MESSAGE_PATTERN.test(message);
366
+ }
367
+ async function runTransaction(client, fn) {
368
+ const session = client.startSession();
369
+ try {
370
+ let result;
371
+ try {
372
+ await session.withTransaction(async () => {
373
+ result = await fn(session);
374
+ });
375
+ return result;
376
+ } catch (error) {
377
+ if (isTransactionsUnsupportedError(error)) {
378
+ return await fn(void 0);
379
+ }
380
+ throw error;
381
+ }
382
+ } finally {
383
+ await session.endSession();
384
+ }
385
+ }
386
+
387
+ // src/db/mongodb/adapter.ts
388
+ var ACTIVE_STATUSES = [...PW_ACTIVE_SUBSCRIPTION_STATUSES];
389
+ function stableStringify(value) {
390
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
391
+ if (value !== null && typeof value === "object") {
392
+ const entries = Object.entries(value).sort(
393
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
394
+ );
395
+ return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
396
+ }
397
+ return JSON.stringify(value);
398
+ }
399
+ var planContent = (p) => ({
400
+ group: p.group,
401
+ isDefault: p.isDefault,
402
+ name: p.name,
403
+ priceMinor: p.priceMinor,
404
+ priceCurrency: p.priceCurrency,
405
+ priceInterval: p.priceInterval,
406
+ features: p.features,
407
+ providerRefs: p.providerRefs
408
+ });
409
+ function planContentEquals(active, input) {
410
+ return stableStringify(planContent(active)) === stableStringify(planContent(input));
411
+ }
412
+ function lit(value) {
413
+ return { $literal: value };
414
+ }
415
+ var SUBSCRIPTION_PATCH_KEYS = [
416
+ "planId",
417
+ "planVersion",
418
+ "status",
419
+ "provider",
420
+ "providerSubscriptionRef",
421
+ "currentPeriodStart",
422
+ "currentPeriodEnd",
423
+ "cancelAtPeriodEnd"
424
+ ];
425
+ function buildAdapter(deps) {
426
+ const sessionOpt = deps.session ? { session: deps.session } : {};
427
+ const customers = deps.db.collection(PW_TABLES.customers);
428
+ const plans = deps.db.collection(PW_TABLES.plans);
429
+ const subscriptions = deps.db.collection(PW_TABLES.subscriptions);
430
+ const balances = deps.db.collection(PW_TABLES.featureBalances);
431
+ const webhookEvents = deps.db.collection(PW_TABLES.webhookEvents);
432
+ return {
433
+ dialect: "mongodb",
434
+ customers: {
435
+ getByExternalId: (externalId) => customersGetByExternalId(customers, externalId, sessionOpt),
436
+ upsert: (input) => customersUpsert(customers, input, sessionOpt),
437
+ linkProviderRef: (externalId, provider, ref) => customersLinkProviderRef(customers, externalId, provider, ref, sessionOpt)
438
+ },
439
+ plans: {
440
+ getActiveVersion: (planId) => plansGetActiveVersion(plans, planId, sessionOpt),
441
+ listActive: () => plansListActive(plans, sessionOpt),
442
+ pushVersion: (plan) => plansPushVersion(plans, plan, sessionOpt)
443
+ },
444
+ subscriptions: {
445
+ getActive: (customerId, group) => subscriptionsGetActive(subscriptions, customerId, group, sessionOpt),
446
+ create: (input) => subscriptionsCreate(subscriptions, input, sessionOpt),
447
+ update: (id, patch) => subscriptionsUpdate(subscriptions, id, patch, sessionOpt)
448
+ },
449
+ balances: {
450
+ get: (customerId, featureId, group) => balancesGet(balances, customerId, featureId, group, sessionOpt),
451
+ consume: (input) => balancesConsume(balances, input, sessionOpt),
452
+ resetTo: (customerId, featureId, group, init) => balancesResetTo(balances, customerId, featureId, group, init, sessionOpt)
453
+ },
454
+ webhookEvents: {
455
+ claim: (dedupeKey, meta) => webhookEventsClaim(webhookEvents, dedupeKey, meta, sessionOpt),
456
+ markApplied: (dedupeKey) => webhookEventsMarkApplied(webhookEvents, dedupeKey, sessionOpt)
457
+ },
458
+ migrations: {
459
+ status: () => mongoMigrationStatus(deps.db),
460
+ apply: () => mongoMigrationApply(deps.db)
461
+ },
462
+ transaction: (fn) => runTransaction(deps.client, (session) => fn(buildAdapter({ ...deps, session })))
463
+ };
464
+ }
465
+ async function customersGetByExternalId(collection, externalId, sessionOpt) {
466
+ const doc = await collection.findOne({ externalId }, sessionOpt);
467
+ return doc ? docToCustomer(doc) : null;
468
+ }
469
+ async function customersUpsert(collection, input, sessionOpt) {
470
+ const now = /* @__PURE__ */ new Date();
471
+ const generatedId = generatePwId(now.getTime());
472
+ const emailExpr = input.email !== void 0 ? lit(input.email) : { $ifNull: ["$email", null] };
473
+ try {
474
+ const doc = await collection.findOneAndUpdate(
475
+ { externalId: input.externalId },
476
+ [
477
+ {
478
+ $set: {
479
+ _id: { $ifNull: ["$_id", lit(generatedId)] },
480
+ externalId: lit(input.externalId),
481
+ providerIds: { $ifNull: ["$providerIds", {}] },
482
+ email: emailExpr,
483
+ createdAt: { $ifNull: ["$createdAt", now] },
484
+ updatedAt: now
485
+ }
486
+ }
487
+ ],
488
+ { upsert: true, returnDocument: "after", ...sessionOpt }
489
+ );
490
+ if (!doc) throw new Error("mongodb adapter: customers.upsert returned no document");
491
+ return docToCustomer(doc);
492
+ } catch (error) {
493
+ throw wrapDriverError(
494
+ error,
495
+ `payweave/db/mongodb: customers.upsert(${JSON.stringify(input.externalId)}) failed.`
496
+ );
497
+ }
498
+ }
499
+ async function customersLinkProviderRef(collection, externalId, provider, ref, sessionOpt) {
500
+ const result = await collection.updateOne(
501
+ { externalId },
502
+ { $set: { [`providerIds.${provider}`]: ref, updatedAt: /* @__PURE__ */ new Date() } },
503
+ sessionOpt
504
+ );
505
+ if (result.matchedCount === 0) {
506
+ throw new PayweaveNotFoundError(
507
+ `payweave/db/mongodb: customers.linkProviderRef: no customer with externalId ${JSON.stringify(externalId)}.`
508
+ );
509
+ }
510
+ }
511
+ async function plansGetActiveVersion(collection, planId, sessionOpt) {
512
+ const doc = await collection.findOne({ planId }, { sort: { version: -1 }, ...sessionOpt });
513
+ return doc ? docToPlanVersion(doc) : null;
514
+ }
515
+ async function plansListActive(collection, sessionOpt) {
516
+ const docs = await collection.aggregate(
517
+ [
518
+ { $sort: { planId: 1, version: -1 } },
519
+ { $group: { _id: "$planId", doc: { $first: "$$ROOT" } } },
520
+ { $replaceRoot: { newRoot: "$doc" } }
521
+ ],
522
+ sessionOpt
523
+ ).toArray();
524
+ return docs.map(docToPlanVersion);
525
+ }
526
+ var PUSH_VERSION_MAX_ATTEMPTS = 5;
527
+ async function plansPushVersion(collection, plan, sessionOpt) {
528
+ for (let attempt = 0; attempt < PUSH_VERSION_MAX_ATTEMPTS; attempt++) {
529
+ const active = await plansGetActiveVersion(collection, plan.planId, sessionOpt);
530
+ if (active && planContentEquals(active, plan)) return active;
531
+ const now = /* @__PURE__ */ new Date();
532
+ const nextVersion = (active?.version ?? 0) + 1;
533
+ const doc = {
534
+ _id: generatePwId(now.getTime()),
535
+ planId: plan.planId,
536
+ version: nextVersion,
537
+ group: plan.group,
538
+ isDefault: plan.isDefault,
539
+ name: plan.name,
540
+ priceMinor: plan.priceMinor,
541
+ priceCurrency: plan.priceCurrency,
542
+ priceInterval: plan.priceInterval,
543
+ features: plan.features,
544
+ providerRefs: plan.providerRefs,
545
+ pushedAt: now
546
+ };
547
+ try {
548
+ await collection.insertOne(doc, sessionOpt);
549
+ return docToPlanVersion(doc);
550
+ } catch (error) {
551
+ if (isDuplicateKeyError(error) && attempt < PUSH_VERSION_MAX_ATTEMPTS - 1) continue;
552
+ throw wrapDriverError(
553
+ error,
554
+ `payweave/db/mongodb: plans.pushVersion(${JSON.stringify(plan.planId)}) failed.`
555
+ );
556
+ }
557
+ }
558
+ throw new Error("payweave/db/mongodb: plans.pushVersion exhausted retries under contention");
559
+ }
560
+ async function subscriptionsGetActive(collection, customerId, group, sessionOpt) {
561
+ const doc = await collection.findOne(
562
+ { customerId, group, status: { $in: ACTIVE_STATUSES } },
563
+ sessionOpt
564
+ );
565
+ return doc ? docToSubscription(doc) : null;
566
+ }
567
+ async function subscriptionsCreate(collection, input, sessionOpt) {
568
+ const now = /* @__PURE__ */ new Date();
569
+ const doc = {
570
+ _id: generatePwId(now.getTime()),
571
+ customerId: input.customerId,
572
+ planId: input.planId,
573
+ planVersion: input.planVersion,
574
+ group: input.group,
575
+ status: input.status,
576
+ provider: input.provider,
577
+ providerSubscriptionRef: input.providerSubscriptionRef,
578
+ currentPeriodStart: input.currentPeriodStart,
579
+ currentPeriodEnd: input.currentPeriodEnd,
580
+ cancelAtPeriodEnd: input.cancelAtPeriodEnd,
581
+ createdAt: now,
582
+ updatedAt: now
583
+ };
584
+ try {
585
+ await collection.insertOne(doc, sessionOpt);
586
+ return docToSubscription(doc);
587
+ } catch (error) {
588
+ throw wrapDriverError(
589
+ error,
590
+ `payweave/db/mongodb: subscriptions.create for customer ${JSON.stringify(input.customerId)} group ${JSON.stringify(input.group)} violated a uniqueness rule (an active-set subscription already exists for this customer/group, or another constraint failed).`
591
+ );
592
+ }
593
+ }
594
+ async function subscriptionsUpdate(collection, id, patch, sessionOpt) {
595
+ const set = { updatedAt: /* @__PURE__ */ new Date() };
596
+ const patchRecord = patch;
597
+ for (const key of SUBSCRIPTION_PATCH_KEYS) {
598
+ if (!(key in patch) || patchRecord[key] === void 0) continue;
599
+ set[key] = patchRecord[key];
600
+ }
601
+ try {
602
+ const doc = await collection.findOneAndUpdate(
603
+ { _id: id },
604
+ { $set: set },
605
+ { returnDocument: "after", ...sessionOpt }
606
+ );
607
+ if (!doc) {
608
+ throw new PayweaveNotFoundError(
609
+ `payweave/db/mongodb: subscriptions.update: no subscription with id ${JSON.stringify(id)}.`
610
+ );
611
+ }
612
+ return docToSubscription(doc);
613
+ } catch (error) {
614
+ if (error instanceof PayweaveNotFoundError) throw error;
615
+ throw wrapDriverError(error, `payweave/db/mongodb: subscriptions.update(${JSON.stringify(id)}) failed.`);
616
+ }
617
+ }
618
+ async function balancesGet(collection, customerId, featureId, group, sessionOpt) {
619
+ const doc = await collection.findOne({ customerId, featureId, group }, sessionOpt);
620
+ return doc ? docToFeatureBalance(doc) : null;
621
+ }
622
+ function buildConsumePipeline(input, generatedId) {
623
+ const nowMs = input.now.getTime();
624
+ const nowLiteral = input.now;
625
+ const conditional = input.conditional === true;
626
+ const amount = input.amount;
627
+ return [
628
+ // A: fill creation defaults. Deliberately does NOT rely on MongoDB
629
+ // auto-seeding the query filter's equality fields into a fresh upserted
630
+ // document (that behavior is a real-server implementation detail this
631
+ // sandbox cannot verify one way or the other) — EVERY field the document
632
+ // needs, including customerId/featureId/group, is explicitly `$ifNull`'d
633
+ // from this call's own inputs (the "$ifNull defaults" pattern),
634
+ // so the pipeline is correct regardless of that assumption.
635
+ {
636
+ $set: {
637
+ _id: { $ifNull: ["$_id", lit(generatedId)] },
638
+ customerId: { $ifNull: ["$customerId", lit(input.customerId)] },
639
+ featureId: { $ifNull: ["$featureId", lit(input.featureId)] },
640
+ group: { $ifNull: ["$group", lit(input.group)] },
641
+ anchor: { $ifNull: ["$anchor", input.init.anchor] },
642
+ resetInterval: { $ifNull: ["$resetInterval", lit(input.init.resetInterval)] },
643
+ limit: { $ifNull: ["$limit", input.init.limit] },
644
+ planId: { $ifNull: ["$planId", lit(input.init.planId)] },
645
+ planVersion: { $ifNull: ["$planVersion", input.init.planVersion] },
646
+ used: { $ifNull: ["$used", 0] }
647
+ }
648
+ },
649
+ // B: the anchor-relative current period (./period-pipeline.ts).
650
+ {
651
+ $set: {
652
+ __period: buildPeriodExpr(nowMs, "$anchor", "$resetInterval")
653
+ }
654
+ },
655
+ // C: reset-due / conditional-gate arithmetic, entirely server-side.
656
+ {
657
+ $set: {
658
+ __hasPeriodEnd: { $ne: [{ $ifNull: ["$periodEnd", null] }, null] }
659
+ }
660
+ },
661
+ {
662
+ $set: {
663
+ __resetDue: {
664
+ $cond: ["$__hasPeriodEnd", { $gte: [nowLiteral, "$periodEnd"] }, true]
665
+ }
666
+ }
667
+ },
668
+ {
669
+ $set: {
670
+ __baseUsed: { $cond: ["$__resetDue", 0, "$used"] }
671
+ }
672
+ },
673
+ // Each of the following stays in its OWN `$set` stage rather than sharing
674
+ // one with the field it depends on — deliberately conservative given this
675
+ // sandbox cannot execute a real pipeline to confirm same-stage field
676
+ // visibility ordering.
677
+ {
678
+ $set: {
679
+ __remaining: { $subtract: ["$limit", "$__baseUsed"] }
680
+ }
681
+ },
682
+ {
683
+ $set: {
684
+ __applied: conditional ? { $gte: ["$__remaining", amount] } : true
685
+ }
686
+ },
687
+ {
688
+ $set: {
689
+ __finalUsed: {
690
+ $cond: ["$__applied", { $add: ["$__baseUsed", amount] }, "$__baseUsed"]
691
+ },
692
+ __shouldWrite: { $or: ["$__applied", "$__resetDue"] }
693
+ }
694
+ },
695
+ // D: write the real fields — untouched (including `updatedAt`) when
696
+ // denied AND no reset was due (leave the row untouched in that case).
697
+ {
698
+ $set: {
699
+ used: { $cond: ["$__shouldWrite", "$__finalUsed", "$used"] },
700
+ periodStart: { $cond: ["$__shouldWrite", "$__period.start", "$periodStart"] },
701
+ periodEnd: { $cond: ["$__shouldWrite", "$__period.end", "$periodEnd"] },
702
+ updatedAt: { $cond: ["$__shouldWrite", nowLiteral, { $ifNull: ["$updatedAt", nowLiteral] }] },
703
+ _pwLastApplied: "$__applied"
704
+ }
705
+ },
706
+ {
707
+ $unset: [
708
+ "__period",
709
+ "__hasPeriodEnd",
710
+ "__resetDue",
711
+ "__baseUsed",
712
+ "__remaining",
713
+ "__applied",
714
+ "__finalUsed",
715
+ "__shouldWrite"
716
+ ]
717
+ }
718
+ ];
719
+ }
720
+ async function balancesConsume(collection, input, sessionOpt) {
721
+ const generatedId = generatePwId(input.now.getTime());
722
+ const pipeline = buildConsumePipeline(input, generatedId);
723
+ const doc = await collection.findOneAndUpdate(
724
+ { customerId: input.customerId, featureId: input.featureId, group: input.group },
725
+ pipeline,
726
+ { upsert: true, returnDocument: "after", ...sessionOpt }
727
+ );
728
+ if (!doc) throw new Error("mongodb adapter: balances.consume returned no document");
729
+ const applied = doc._pwLastApplied ?? true;
730
+ return { ...docToFeatureBalance(doc), applied };
731
+ }
732
+ async function balancesResetTo(collection, customerId, featureId, group, init, sessionOpt) {
733
+ const now = /* @__PURE__ */ new Date();
734
+ const period = currentPeriod(init.anchor.getTime(), init.resetInterval, now.getTime());
735
+ const generatedId = generatePwId(now.getTime());
736
+ await collection.findOneAndUpdate(
737
+ { customerId, featureId, group },
738
+ [
739
+ {
740
+ $set: {
741
+ _id: { $ifNull: ["$_id", lit(generatedId)] },
742
+ // Same defensive explicitness as `buildConsumePipeline` — never
743
+ // rely on MongoDB auto-seeding the filter's equality fields, and
744
+ // never let a caller-supplied string be misread as a field path.
745
+ customerId: lit(customerId),
746
+ featureId: lit(featureId),
747
+ group: lit(group),
748
+ used: 0,
749
+ limit: init.limit,
750
+ resetInterval: lit(init.resetInterval),
751
+ anchor: init.anchor,
752
+ periodStart: new Date(period.start),
753
+ periodEnd: new Date(period.end),
754
+ planId: lit(init.planId),
755
+ planVersion: init.planVersion,
756
+ updatedAt: now
757
+ }
758
+ }
759
+ ],
760
+ { upsert: true, ...sessionOpt }
761
+ );
762
+ }
763
+ function buildClaimPipeline(dedupeKey, meta) {
764
+ const staleClaimAfterMs = meta.staleClaimAfterMs ?? DEFAULT_STALE_CLAIM_AFTER_MS;
765
+ const staleThreshold = new Date(meta.now.getTime() - staleClaimAfterMs);
766
+ return [
767
+ {
768
+ $set: {
769
+ // Same defensive explicitness as `buildConsumePipeline` — never rely
770
+ // on MongoDB auto-seeding the `_id` filter's equality match on
771
+ // upsert, and never let a caller-supplied string be misread as a
772
+ // field path.
773
+ _id: { $ifNull: ["$_id", lit(dedupeKey)] },
774
+ provider: { $ifNull: ["$provider", lit(meta.provider)] },
775
+ type: { $ifNull: ["$type", lit(meta.type)] },
776
+ receivedAt: { $ifNull: ["$receivedAt", meta.now] },
777
+ appliedAt: { $ifNull: ["$appliedAt", null] },
778
+ claimedAt: { $ifNull: ["$claimedAt", null] }
779
+ }
780
+ },
781
+ {
782
+ $set: {
783
+ _pwWon: {
784
+ $and: [
785
+ { $eq: ["$appliedAt", null] },
786
+ { $or: [{ $eq: ["$claimedAt", null] }, { $lte: ["$claimedAt", staleThreshold] }] }
787
+ ]
788
+ }
789
+ }
790
+ },
791
+ {
792
+ $set: {
793
+ claimedAt: { $cond: ["$_pwWon", meta.now, "$claimedAt"] }
794
+ }
795
+ }
796
+ ];
797
+ }
798
+ async function webhookEventsClaim(collection, dedupeKey, meta, sessionOpt) {
799
+ const pipeline = buildClaimPipeline(dedupeKey, meta);
800
+ const doc = await collection.findOneAndUpdate(
801
+ { _id: dedupeKey },
802
+ pipeline,
803
+ { upsert: true, returnDocument: "after", ...sessionOpt }
804
+ );
805
+ return doc?._pwWon === true;
806
+ }
807
+ async function webhookEventsMarkApplied(collection, dedupeKey, sessionOpt) {
808
+ await collection.updateOne({ _id: dedupeKey }, { $set: { appliedAt: /* @__PURE__ */ new Date() } }, sessionOpt);
809
+ }
810
+
811
+ // src/db/mongodb/url.ts
812
+ var URL_SCHEMES = ["mongodb://", "mongodb+srv://"];
813
+ function looksLikeMongoClient(value) {
814
+ const candidate = value;
815
+ return typeof candidate.connect === "function" && typeof candidate.close === "function" && typeof candidate.db === "function" && typeof candidate.startSession === "function";
816
+ }
817
+ function validateMongoUrl(url) {
818
+ if (typeof url !== "string" || url.length === 0) {
819
+ throw new PayweaveConfigError(
820
+ "payweave/db/mongodb: mongodbAdapter({ url, dbName }) requires a non-empty string url."
821
+ );
822
+ }
823
+ if (!URL_SCHEMES.some((scheme) => url.startsWith(scheme))) {
824
+ throw new PayweaveConfigError(
825
+ 'payweave/db/mongodb: mongodbAdapter({ url, ... }) requires a "mongodb://" or "mongodb+srv://" connection string.'
826
+ );
827
+ }
828
+ }
829
+ function validateDbName(dbName) {
830
+ if (typeof dbName !== "string" || dbName.length === 0) {
831
+ throw new PayweaveConfigError(
832
+ "payweave/db/mongodb: mongodbAdapter(...) requires a non-empty `dbName` \u2014 a MongoDB connection string may name a default database in its path, but this adapter always requires `dbName` explicitly, for both { url } and { client }."
833
+ );
834
+ }
835
+ }
836
+ function resolveMongoInput(input) {
837
+ if (input === null || typeof input !== "object") {
838
+ throw new PayweaveConfigError(
839
+ `payweave/db/mongodb: mongodbAdapter(...) expects { url, dbName } (a mongodb:///mongodb+srv:// connection string) or { client, dbName } (an existing MongoClient) \u2014 got ${input === null ? "null" : typeof input}.`
840
+ );
841
+ }
842
+ const record = input;
843
+ if ("url" in record) {
844
+ validateMongoUrl(record.url);
845
+ validateDbName(record.dbName);
846
+ return { kind: "url", url: record.url, dbName: record.dbName };
847
+ }
848
+ if ("client" in record) {
849
+ const client = record.client;
850
+ if (client === null || typeof client !== "object" || !looksLikeMongoClient(client)) {
851
+ throw new PayweaveConfigError(
852
+ `payweave/db/mongodb: mongodbAdapter({ client, dbName }) expects \`client\` to be a MongoClient-like instance (has .connect()/.close()/.db()/.startSession()) \u2014 got ${client === null ? "null" : typeof client === "object" ? "an object missing one of those methods" : typeof client}.`
853
+ );
854
+ }
855
+ validateDbName(record.dbName);
856
+ return { kind: "client", client, dbName: record.dbName };
857
+ }
858
+ throw new PayweaveConfigError(
859
+ "payweave/db/mongodb: mongodbAdapter(...) expects { url, dbName } (a mongodb:///mongodb+srv:// connection string) or { client, dbName } (an existing MongoClient) \u2014 got an object matching neither shape."
860
+ );
861
+ }
862
+
863
+ // src/db/mongodb/index.ts
864
+ async function connect(target) {
865
+ if (target.kind === "client") {
866
+ const client2 = target.client;
867
+ await target.client.connect();
868
+ return { client: client2, db: client2.db(target.dbName) };
869
+ }
870
+ let mod;
871
+ try {
872
+ mod = await import('mongodb');
873
+ } catch (cause) {
874
+ throw installHintError(cause);
875
+ }
876
+ const rawClient = new mod.MongoClient(target.url);
877
+ await rawClient.connect();
878
+ const client = rawClient;
879
+ return { client, db: client.db(target.dbName) };
880
+ }
881
+ function buildLazyAdapter(getConnection) {
882
+ let builtPromise;
883
+ const getReal = () => {
884
+ builtPromise ??= getConnection().then(
885
+ ({ client, db }) => buildAdapter({ client, db })
886
+ );
887
+ return builtPromise;
888
+ };
889
+ return {
890
+ dialect: "mongodb",
891
+ customers: {
892
+ getByExternalId: async (externalId) => (await getReal()).customers.getByExternalId(externalId),
893
+ upsert: async (input) => (await getReal()).customers.upsert(input),
894
+ linkProviderRef: async (externalId, provider, ref) => (await getReal()).customers.linkProviderRef(externalId, provider, ref)
895
+ },
896
+ plans: {
897
+ getActiveVersion: async (planId) => (await getReal()).plans.getActiveVersion(planId),
898
+ listActive: async () => (await getReal()).plans.listActive(),
899
+ pushVersion: async (plan) => (await getReal()).plans.pushVersion(plan)
900
+ },
901
+ subscriptions: {
902
+ getActive: async (customerId, group) => (await getReal()).subscriptions.getActive(customerId, group),
903
+ create: async (input) => (await getReal()).subscriptions.create(input),
904
+ update: async (id, patch) => (await getReal()).subscriptions.update(id, patch)
905
+ },
906
+ balances: {
907
+ get: async (customerId, featureId, group) => (await getReal()).balances.get(customerId, featureId, group),
908
+ consume: async (input) => (await getReal()).balances.consume(input),
909
+ resetTo: async (customerId, featureId, group, init) => (await getReal()).balances.resetTo(customerId, featureId, group, init)
910
+ },
911
+ webhookEvents: {
912
+ claim: async (dedupeKey, meta) => (await getReal()).webhookEvents.claim(dedupeKey, meta),
913
+ markApplied: async (dedupeKey) => (await getReal()).webhookEvents.markApplied(dedupeKey)
914
+ },
915
+ migrations: {
916
+ status: async () => (await getReal()).migrations.status(),
917
+ apply: async () => (await getReal()).migrations.apply()
918
+ },
919
+ transaction: async (fn) => (await getReal()).transaction(fn)
920
+ };
921
+ }
922
+ function mongodbAdapter(input) {
923
+ const target = resolveMongoInput(input);
924
+ let connectPromise;
925
+ const getConnection = () => {
926
+ connectPromise ??= connect(target);
927
+ return connectPromise;
928
+ };
929
+ return buildLazyAdapter(getConnection);
930
+ }
931
+
932
+ export { mongodbAdapter };
933
+ //# sourceMappingURL=index.js.map
934
+ //# sourceMappingURL=index.js.map