@classytic/revenue 1.1.2 → 1.1.3

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/README.md +8 -7
  2. package/dist/application/services/index.d.mts +4 -0
  3. package/dist/application/services/index.mjs +3 -0
  4. package/dist/base-CsTlVQJe.d.mts +136 -0
  5. package/dist/base-DCoyIUj6.mjs +152 -0
  6. package/dist/category-resolver-DV83N8ok.mjs +284 -0
  7. package/dist/commission-split-BzB8cd39.mjs +485 -0
  8. package/dist/core/events.d.mts +294 -0
  9. package/dist/core/events.mjs +100 -0
  10. package/dist/core/index.d.mts +9 -0
  11. package/dist/core/index.mjs +8 -0
  12. package/dist/enums/index.d.mts +157 -0
  13. package/dist/enums/index.mjs +56 -0
  14. package/dist/errors-CorrWz7A.d.mts +787 -0
  15. package/dist/escrow.enums-CZGrrdg7.mjs +101 -0
  16. package/dist/{escrow.enums-CE0VQsfe.d.ts → escrow.enums-DwdLuuve.d.mts} +30 -28
  17. package/dist/idempotency-DaYcUGY1.mjs +172 -0
  18. package/dist/index-Dsp7H5Wb.d.mts +471 -0
  19. package/dist/index.d.mts +9 -0
  20. package/dist/index.mjs +38 -0
  21. package/dist/infrastructure/plugins/{index.d.ts → index.d.mts} +81 -109
  22. package/dist/infrastructure/plugins/index.mjs +345 -0
  23. package/dist/money-CvrDOijQ.mjs +271 -0
  24. package/dist/money-DPG8AtJ8.d.mts +112 -0
  25. package/dist/{payment.enums-C1BiGlRa.d.ts → payment.enums-HAuAS9Pp.d.mts} +14 -13
  26. package/dist/payment.enums-tEFVa-Xp.mjs +69 -0
  27. package/dist/plugin-BbK0OVHy.d.mts +327 -0
  28. package/dist/plugin-Cd_V04Em.mjs +210 -0
  29. package/dist/providers/index.d.mts +3 -0
  30. package/dist/providers/index.mjs +3 -0
  31. package/dist/reconciliation/{index.d.ts → index.d.mts} +90 -112
  32. package/dist/reconciliation/index.mjs +192 -0
  33. package/dist/retry-HHCOXYdn.d.mts +186 -0
  34. package/dist/revenue-9scqKSef.mjs +553 -0
  35. package/dist/schemas/index.d.mts +2665 -0
  36. package/dist/schemas/index.mjs +717 -0
  37. package/dist/schemas/validation.d.mts +375 -0
  38. package/dist/schemas/validation.mjs +325 -0
  39. package/dist/{settlement.enums-ByC1x0ye.d.ts → settlement.enums-DFhkqZEY.d.mts} +31 -29
  40. package/dist/settlement.schema-D5uWB5tP.d.mts +344 -0
  41. package/dist/settlement.service-BxuiHpNC.d.mts +594 -0
  42. package/dist/settlement.service-CUxbUTzT.mjs +2510 -0
  43. package/dist/split.enums-BrjabxIX.mjs +86 -0
  44. package/dist/split.enums-DmskfLOM.d.mts +43 -0
  45. package/dist/tax-BoCt5cEd.d.mts +61 -0
  46. package/dist/tax-EQ15DO81.mjs +162 -0
  47. package/dist/transaction.enums-pCyMFT4Z.mjs +96 -0
  48. package/dist/utils/{index.d.ts → index.d.mts} +91 -161
  49. package/dist/utils/index.mjs +346 -0
  50. package/package.json +38 -36
  51. package/dist/application/services/index.d.ts +0 -6
  52. package/dist/application/services/index.js +0 -3288
  53. package/dist/application/services/index.js.map +0 -1
  54. package/dist/core/events.d.ts +0 -455
  55. package/dist/core/events.js +0 -122
  56. package/dist/core/events.js.map +0 -1
  57. package/dist/core/index.d.ts +0 -13
  58. package/dist/core/index.js +0 -4591
  59. package/dist/core/index.js.map +0 -1
  60. package/dist/enums/index.d.ts +0 -159
  61. package/dist/enums/index.js +0 -296
  62. package/dist/enums/index.js.map +0 -1
  63. package/dist/index-DxIK0UmZ.d.ts +0 -633
  64. package/dist/index-EnfKzDbs.d.ts +0 -806
  65. package/dist/index-cLJBLUvx.d.ts +0 -478
  66. package/dist/index.d.ts +0 -43
  67. package/dist/index.js +0 -4864
  68. package/dist/index.js.map +0 -1
  69. package/dist/infrastructure/plugins/index.js +0 -292
  70. package/dist/infrastructure/plugins/index.js.map +0 -1
  71. package/dist/money-widWVD7r.d.ts +0 -111
  72. package/dist/plugin-Bb9HOE10.d.ts +0 -336
  73. package/dist/providers/index.d.ts +0 -145
  74. package/dist/providers/index.js +0 -141
  75. package/dist/providers/index.js.map +0 -1
  76. package/dist/reconciliation/index.js +0 -140
  77. package/dist/reconciliation/index.js.map +0 -1
  78. package/dist/retry-D4hFUwVk.d.ts +0 -194
  79. package/dist/schemas/index.d.ts +0 -2655
  80. package/dist/schemas/index.js +0 -841
  81. package/dist/schemas/index.js.map +0 -1
  82. package/dist/schemas/validation.d.ts +0 -384
  83. package/dist/schemas/validation.js +0 -303
  84. package/dist/schemas/validation.js.map +0 -1
  85. package/dist/settlement.schema-CpamV7ZY.d.ts +0 -343
  86. package/dist/split.enums-DG3TxQf9.d.ts +0 -42
  87. package/dist/tax-CV8A0sxl.d.ts +0 -60
  88. package/dist/utils/index.js +0 -1202
  89. package/dist/utils/index.js.map +0 -1
@@ -1,4591 +0,0 @@
1
- import { nanoid } from 'nanoid';
2
-
3
- // @classytic/revenue - Enterprise Revenue Management System
4
-
5
-
6
- // src/core/container.ts
7
- var Container = class _Container {
8
- _services;
9
- _singletons;
10
- constructor() {
11
- this._services = /* @__PURE__ */ new Map();
12
- this._singletons = /* @__PURE__ */ new Map();
13
- }
14
- /**
15
- * Register a service
16
- * @param name - Service name
17
- * @param implementation - Service implementation or factory
18
- * @param options - Registration options
19
- */
20
- register(name, implementation, options = {}) {
21
- this._services.set(name, {
22
- implementation,
23
- singleton: options.singleton !== false,
24
- // Default to singleton
25
- factory: options.factory ?? false
26
- });
27
- return this;
28
- }
29
- /**
30
- * Register a singleton service
31
- * @param name - Service name
32
- * @param implementation - Service implementation
33
- */
34
- singleton(name, implementation) {
35
- return this.register(name, implementation, { singleton: true });
36
- }
37
- /**
38
- * Register a transient service (new instance each time)
39
- * @param name - Service name
40
- * @param factory - Factory function
41
- */
42
- transient(name, factory) {
43
- return this.register(name, factory, { singleton: false, factory: true });
44
- }
45
- /**
46
- * Get a service from the container
47
- * @param name - Service name
48
- * @returns Service instance
49
- */
50
- get(name) {
51
- if (this._singletons.has(name)) {
52
- return this._singletons.get(name);
53
- }
54
- const service = this._services.get(name);
55
- if (!service) {
56
- throw new Error(`Service "${name}" not registered in container`);
57
- }
58
- if (service.factory) {
59
- const factory = service.implementation;
60
- const instance2 = factory(this);
61
- if (service.singleton) {
62
- this._singletons.set(name, instance2);
63
- }
64
- return instance2;
65
- }
66
- const instance = service.implementation;
67
- if (service.singleton) {
68
- this._singletons.set(name, instance);
69
- }
70
- return instance;
71
- }
72
- /**
73
- * Check if service is registered
74
- * @param name - Service name
75
- */
76
- has(name) {
77
- return this._services.has(name);
78
- }
79
- /**
80
- * Get all registered service names
81
- */
82
- keys() {
83
- return Array.from(this._services.keys());
84
- }
85
- /**
86
- * Clear all services (useful for testing)
87
- */
88
- clear() {
89
- this._services.clear();
90
- this._singletons.clear();
91
- }
92
- /**
93
- * Create a child container (for scoped dependencies)
94
- */
95
- createScope() {
96
- const scope = new _Container();
97
- this._services.forEach((value, key) => {
98
- scope._services.set(key, value);
99
- });
100
- return scope;
101
- }
102
- };
103
-
104
- // src/core/events.ts
105
- var EventBus = class {
106
- handlers = /* @__PURE__ */ new Map();
107
- onceHandlers = /* @__PURE__ */ new Map();
108
- /**
109
- * Subscribe to an event
110
- */
111
- on(event, handler) {
112
- if (!this.handlers.has(event)) {
113
- this.handlers.set(event, /* @__PURE__ */ new Set());
114
- }
115
- this.handlers.get(event).add(handler);
116
- return () => this.off(event, handler);
117
- }
118
- /**
119
- * Subscribe to an event once
120
- */
121
- once(event, handler) {
122
- if (!this.onceHandlers.has(event)) {
123
- this.onceHandlers.set(event, /* @__PURE__ */ new Set());
124
- }
125
- this.onceHandlers.get(event).add(handler);
126
- return () => this.onceHandlers.get(event)?.delete(handler);
127
- }
128
- /**
129
- * Unsubscribe from an event
130
- */
131
- off(event, handler) {
132
- this.handlers.get(event)?.delete(handler);
133
- this.onceHandlers.get(event)?.delete(handler);
134
- }
135
- emit(event, data) {
136
- const fullPayload = {
137
- ...data,
138
- type: event,
139
- timestamp: /* @__PURE__ */ new Date()
140
- };
141
- const handlers = this.handlers.get(event);
142
- if (handlers) {
143
- for (const handler of handlers) {
144
- Promise.resolve(handler(fullPayload)).catch((err2) => {
145
- console.error(`[Revenue] Event handler error for "${event}":`, err2);
146
- });
147
- }
148
- }
149
- const onceHandlers = this.onceHandlers.get(event);
150
- if (onceHandlers) {
151
- for (const handler of onceHandlers) {
152
- Promise.resolve(handler(fullPayload)).catch((err2) => {
153
- console.error(`[Revenue] Once handler error for "${event}":`, err2);
154
- });
155
- }
156
- this.onceHandlers.delete(event);
157
- }
158
- if (event !== "*") {
159
- const wildcardHandlers = this.handlers.get("*");
160
- if (wildcardHandlers) {
161
- for (const handler of wildcardHandlers) {
162
- Promise.resolve(handler(fullPayload)).catch((err2) => {
163
- console.error(`[Revenue] Wildcard handler error:`, err2);
164
- });
165
- }
166
- }
167
- }
168
- }
169
- /**
170
- * Emit and wait for all handlers to complete
171
- */
172
- async emitAsync(event, payload) {
173
- const fullPayload = {
174
- ...payload,
175
- type: event,
176
- timestamp: /* @__PURE__ */ new Date()
177
- };
178
- const promises = [];
179
- const handlers = this.handlers.get(event);
180
- if (handlers) {
181
- for (const handler of handlers) {
182
- promises.push(Promise.resolve(handler(fullPayload)));
183
- }
184
- }
185
- const onceHandlers = this.onceHandlers.get(event);
186
- if (onceHandlers) {
187
- for (const handler of onceHandlers) {
188
- promises.push(Promise.resolve(handler(fullPayload)));
189
- }
190
- this.onceHandlers.delete(event);
191
- }
192
- if (event !== "*") {
193
- const wildcardHandlers = this.handlers.get("*");
194
- if (wildcardHandlers) {
195
- for (const handler of wildcardHandlers) {
196
- promises.push(Promise.resolve(handler(fullPayload)));
197
- }
198
- }
199
- }
200
- await Promise.all(promises);
201
- }
202
- /**
203
- * Remove all handlers
204
- */
205
- clear() {
206
- this.handlers.clear();
207
- this.onceHandlers.clear();
208
- }
209
- /**
210
- * Get handler count for an event
211
- */
212
- listenerCount(event) {
213
- return (this.handlers.get(event)?.size ?? 0) + (this.onceHandlers.get(event)?.size ?? 0);
214
- }
215
- };
216
- function createEventBus() {
217
- return new EventBus();
218
- }
219
-
220
- // src/core/plugin.ts
221
- var PluginManager = class {
222
- plugins = /* @__PURE__ */ new Map();
223
- hooks = /* @__PURE__ */ new Map();
224
- initialized = false;
225
- /**
226
- * Register a plugin
227
- */
228
- register(plugin) {
229
- if (this.plugins.has(plugin.name)) {
230
- throw new Error(`Plugin "${plugin.name}" is already registered`);
231
- }
232
- if (plugin.dependencies) {
233
- for (const dep of plugin.dependencies) {
234
- if (!this.plugins.has(dep)) {
235
- throw new Error(
236
- `Plugin "${plugin.name}" requires "${dep}" to be registered first`
237
- );
238
- }
239
- }
240
- }
241
- this.plugins.set(plugin.name, plugin);
242
- if (plugin.hooks) {
243
- for (const [hookName, hookFn] of Object.entries(plugin.hooks)) {
244
- if (!this.hooks.has(hookName)) {
245
- this.hooks.set(hookName, []);
246
- }
247
- this.hooks.get(hookName).push(hookFn);
248
- }
249
- }
250
- return this;
251
- }
252
- /**
253
- * Initialize all plugins
254
- */
255
- async init(ctx) {
256
- if (this.initialized) return;
257
- for (const plugin of this.plugins.values()) {
258
- if (plugin.init) {
259
- await plugin.init(ctx);
260
- }
261
- if (plugin.events) {
262
- for (const [event, handler] of Object.entries(plugin.events)) {
263
- ctx.events.on(event, handler);
264
- }
265
- }
266
- }
267
- this.initialized = true;
268
- }
269
- /**
270
- * Execute a hook chain
271
- */
272
- async executeHook(hookName, ctx, input, execute) {
273
- const hooks = this.hooks.get(hookName) ?? [];
274
- if (hooks.length === 0) {
275
- return execute();
276
- }
277
- let index = 0;
278
- const next = async () => {
279
- if (index >= hooks.length) {
280
- return execute();
281
- }
282
- const hook = hooks[index++];
283
- return hook(ctx, input, next);
284
- };
285
- return next();
286
- }
287
- /**
288
- * Check if plugin is registered
289
- */
290
- has(name) {
291
- return this.plugins.has(name);
292
- }
293
- /**
294
- * Get a plugin by name
295
- */
296
- get(name) {
297
- return this.plugins.get(name);
298
- }
299
- /**
300
- * Get all registered plugins
301
- */
302
- list() {
303
- return Array.from(this.plugins.values());
304
- }
305
- /**
306
- * Destroy all plugins
307
- */
308
- async destroy() {
309
- for (const plugin of this.plugins.values()) {
310
- if (plugin.destroy) {
311
- await plugin.destroy();
312
- }
313
- }
314
- this.plugins.clear();
315
- this.hooks.clear();
316
- this.initialized = false;
317
- }
318
- };
319
- function loggingPlugin(options = {}) {
320
- const level = options.level ?? "info";
321
- return {
322
- name: "logging",
323
- version: "1.0.0",
324
- description: "Logs all revenue operations",
325
- hooks: {
326
- "payment.create.after": async (ctx, input, next) => {
327
- ctx.logger[level]("Creating payment", { amount: input.amount, currency: input.currency });
328
- const result = await next();
329
- ctx.logger[level]("Payment created", { paymentIntentId: result?.paymentIntentId });
330
- return result;
331
- },
332
- "payment.verify.after": async (ctx, input, next) => {
333
- ctx.logger[level]("Verifying payment", { id: input.id });
334
- const result = await next();
335
- ctx.logger[level]("Payment verified", { verified: result?.verified });
336
- return result;
337
- },
338
- "payment.refund.after": async (ctx, input, next) => {
339
- ctx.logger[level]("Processing refund", { transactionId: input.transactionId, amount: input.amount });
340
- const result = await next();
341
- ctx.logger[level]("Refund processed", { refundId: result?.refundId });
342
- return result;
343
- }
344
- }
345
- };
346
- }
347
- function auditPlugin(options = {}) {
348
- const store = options.store ?? (async (entry) => {
349
- });
350
- return {
351
- name: "audit",
352
- version: "1.0.0",
353
- description: "Audit trail for all operations",
354
- hooks: {
355
- "payment.create.after": async (ctx, input, next) => {
356
- const result = await next();
357
- await store({
358
- action: "payment.create",
359
- requestId: ctx.meta.requestId,
360
- timestamp: ctx.meta.timestamp,
361
- input: sanitizeInput(input),
362
- output: sanitizeOutput(result),
363
- idempotencyKey: ctx.meta.idempotencyKey
364
- });
365
- return result;
366
- },
367
- "payment.refund.after": async (ctx, input, next) => {
368
- const result = await next();
369
- await store({
370
- action: "payment.refund",
371
- requestId: ctx.meta.requestId,
372
- timestamp: ctx.meta.timestamp,
373
- input: sanitizeInput(input),
374
- output: sanitizeOutput(result),
375
- idempotencyKey: ctx.meta.idempotencyKey
376
- });
377
- return result;
378
- }
379
- }
380
- };
381
- }
382
- function sanitizeInput(input) {
383
- if (typeof input !== "object" || !input) return {};
384
- const sanitized = { ...input };
385
- delete sanitized.apiKey;
386
- delete sanitized.secretKey;
387
- delete sanitized.password;
388
- return sanitized;
389
- }
390
- function sanitizeOutput(output) {
391
- if (typeof output !== "object" || !output) return {};
392
- return { ...output };
393
- }
394
- function metricsPlugin(options = {}) {
395
- const record = options.onMetric ?? ((metric) => {
396
- });
397
- return {
398
- name: "metrics",
399
- version: "1.0.0",
400
- description: "Collects operation metrics",
401
- hooks: {
402
- "payment.create.before": async (_ctx, input, next) => {
403
- const start = Date.now();
404
- try {
405
- const result = await next();
406
- record({
407
- name: "payment.create",
408
- duration: Date.now() - start,
409
- success: true,
410
- amount: input.amount,
411
- currency: input.currency
412
- });
413
- return result;
414
- } catch (error) {
415
- record({
416
- name: "payment.create",
417
- duration: Date.now() - start,
418
- success: false,
419
- error: error.message
420
- });
421
- throw error;
422
- }
423
- }
424
- }
425
- };
426
- }
427
- function definePlugin(plugin) {
428
- return plugin;
429
- }
430
-
431
- // src/core/result.ts
432
- function ok(value) {
433
- return { ok: true, value };
434
- }
435
- function err(error) {
436
- return { ok: false, error };
437
- }
438
- function isOk(result) {
439
- return result.ok === true;
440
- }
441
- function isErr(result) {
442
- return result.ok === false;
443
- }
444
- function unwrap(result) {
445
- if (result.ok) return result.value;
446
- throw result.error;
447
- }
448
- function unwrapOr(result, defaultValue) {
449
- return result.ok ? result.value : defaultValue;
450
- }
451
- function map(result, fn) {
452
- return result.ok ? ok(fn(result.value)) : result;
453
- }
454
- function mapErr(result, fn) {
455
- return result.ok ? result : err(fn(result.error));
456
- }
457
- function flatMap(result, fn) {
458
- return result.ok ? fn(result.value) : result;
459
- }
460
- async function tryCatch(fn, mapError) {
461
- try {
462
- const value = await fn();
463
- return ok(value);
464
- } catch (e) {
465
- const error = mapError ? mapError(e) : e;
466
- return err(error);
467
- }
468
- }
469
- function tryCatchSync(fn, mapError) {
470
- try {
471
- const value = fn();
472
- return ok(value);
473
- } catch (e) {
474
- const error = mapError ? mapError(e) : e;
475
- return err(error);
476
- }
477
- }
478
- function all(results) {
479
- const values = [];
480
- for (const result of results) {
481
- if (!result.ok) return result;
482
- values.push(result.value);
483
- }
484
- return ok(values);
485
- }
486
- function match(result, handlers) {
487
- return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
488
- }
489
- var Result = {
490
- ok,
491
- err,
492
- isOk,
493
- isErr,
494
- unwrap,
495
- unwrapOr,
496
- map,
497
- mapErr,
498
- flatMap,
499
- tryCatch,
500
- tryCatchSync,
501
- all,
502
- match
503
- };
504
- var MemoryIdempotencyStore = class {
505
- records = /* @__PURE__ */ new Map();
506
- cleanupInterval = null;
507
- constructor(cleanupIntervalMs = 6e4) {
508
- this.cleanupInterval = setInterval(() => {
509
- this.cleanup();
510
- }, cleanupIntervalMs);
511
- }
512
- async get(key) {
513
- const record = this.records.get(key);
514
- if (!record) return null;
515
- if (record.expiresAt < /* @__PURE__ */ new Date()) {
516
- this.records.delete(key);
517
- return null;
518
- }
519
- return record;
520
- }
521
- async set(key, record) {
522
- this.records.set(key, record);
523
- }
524
- async delete(key) {
525
- this.records.delete(key);
526
- }
527
- async exists(key) {
528
- const record = await this.get(key);
529
- return record !== null;
530
- }
531
- cleanup() {
532
- const now = /* @__PURE__ */ new Date();
533
- for (const [key, record] of this.records) {
534
- if (record.expiresAt < now) {
535
- this.records.delete(key);
536
- }
537
- }
538
- }
539
- destroy() {
540
- if (this.cleanupInterval) {
541
- clearInterval(this.cleanupInterval);
542
- this.cleanupInterval = null;
543
- }
544
- this.records.clear();
545
- }
546
- };
547
- var IdempotencyError = class extends Error {
548
- constructor(message, code) {
549
- super(message);
550
- this.code = code;
551
- this.name = "IdempotencyError";
552
- }
553
- };
554
- var IdempotencyManager = class {
555
- store;
556
- ttl;
557
- prefix;
558
- constructor(config = {}) {
559
- this.store = config.store ?? new MemoryIdempotencyStore();
560
- this.ttl = config.ttl ?? 24 * 60 * 60 * 1e3;
561
- this.prefix = config.prefix ?? "idem:";
562
- }
563
- /**
564
- * Generate a unique idempotency key
565
- */
566
- generateKey() {
567
- return `${this.prefix}${nanoid(21)}`;
568
- }
569
- /**
570
- * Hash request parameters for validation
571
- * Uses deterministic JSON serialization and simple hash function
572
- */
573
- hashRequest(params) {
574
- let normalized;
575
- if (params === null || params === void 0) {
576
- normalized = null;
577
- } else if (typeof params === "object" && !Array.isArray(params)) {
578
- const sortedKeys = Object.keys(params).sort();
579
- normalized = sortedKeys.reduce((acc, key) => {
580
- acc[key] = params[key];
581
- return acc;
582
- }, {});
583
- } else {
584
- normalized = params;
585
- }
586
- const json = JSON.stringify(normalized);
587
- let hash = 0;
588
- for (let i = 0; i < json.length; i++) {
589
- const char = json.charCodeAt(i);
590
- hash = (hash << 5) - hash + char;
591
- hash = hash & hash;
592
- }
593
- return Math.abs(hash).toString(36);
594
- }
595
- /**
596
- * Execute operation with idempotency protection
597
- */
598
- async execute(key, params, operation) {
599
- const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
600
- const requestHash = this.hashRequest(params);
601
- const existing = await this.store.get(fullKey);
602
- if (existing) {
603
- if (existing.requestHash !== requestHash) {
604
- return err(new IdempotencyError(
605
- "Idempotency key used with different request parameters",
606
- "REQUEST_MISMATCH"
607
- ));
608
- }
609
- if (existing.status === "completed" && existing.result !== void 0) {
610
- return ok(existing.result);
611
- }
612
- if (existing.status === "pending") {
613
- return err(new IdempotencyError(
614
- "Request with this idempotency key is already in progress",
615
- "REQUEST_IN_PROGRESS"
616
- ));
617
- }
618
- if (existing.status === "failed") {
619
- await this.store.delete(fullKey);
620
- }
621
- }
622
- const record = {
623
- key: fullKey,
624
- status: "pending",
625
- createdAt: /* @__PURE__ */ new Date(),
626
- requestHash,
627
- expiresAt: new Date(Date.now() + this.ttl)
628
- };
629
- await this.store.set(fullKey, record);
630
- try {
631
- const result = await operation();
632
- record.status = "completed";
633
- record.result = result;
634
- record.completedAt = /* @__PURE__ */ new Date();
635
- await this.store.set(fullKey, record);
636
- return ok(result);
637
- } catch (error) {
638
- record.status = "failed";
639
- await this.store.set(fullKey, record);
640
- throw error;
641
- }
642
- }
643
- /**
644
- * Check if operation with key was already completed
645
- */
646
- async wasCompleted(key) {
647
- const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
648
- const record = await this.store.get(fullKey);
649
- return record?.status === "completed";
650
- }
651
- /**
652
- * Get cached result for key
653
- */
654
- async getCached(key) {
655
- const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
656
- const record = await this.store.get(fullKey);
657
- return record?.status === "completed" ? record.result ?? null : null;
658
- }
659
- /**
660
- * Invalidate a key (force re-execution on next call)
661
- */
662
- async invalidate(key) {
663
- const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
664
- await this.store.delete(fullKey);
665
- }
666
- /**
667
- * Destroy the idempotency manager and cleanup resources
668
- * Call this when shutting down to prevent memory leaks
669
- */
670
- destroy() {
671
- if (this.store instanceof MemoryIdempotencyStore) {
672
- this.store.destroy();
673
- }
674
- }
675
- };
676
- function createIdempotencyManager(config) {
677
- return new IdempotencyManager(config);
678
- }
679
-
680
- // src/shared/utils/resilience/retry.ts
681
- var DEFAULT_CONFIG = {
682
- maxAttempts: 3,
683
- baseDelay: 1e3,
684
- maxDelay: 3e4,
685
- backoffMultiplier: 2,
686
- jitter: 0.1,
687
- retryIf: isRetryableError
688
- };
689
- function calculateDelay(attempt, config) {
690
- const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
691
- const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
692
- const jitterRange = cappedDelay * config.jitter;
693
- const jitter = Math.random() * jitterRange * 2 - jitterRange;
694
- return Math.round(Math.max(0, cappedDelay + jitter));
695
- }
696
- function sleep(ms) {
697
- return new Promise((resolve) => setTimeout(resolve, ms));
698
- }
699
- function isRetryableError(error) {
700
- if (!(error instanceof Error)) return false;
701
- if (error.message.includes("ECONNREFUSED")) return true;
702
- if (error.message.includes("ETIMEDOUT")) return true;
703
- if (error.message.includes("ENOTFOUND")) return true;
704
- if (error.message.includes("network")) return true;
705
- if (error.message.includes("timeout")) return true;
706
- if (error.message.includes("429")) return true;
707
- if (error.message.includes("rate limit")) return true;
708
- if (error.message.includes("500")) return true;
709
- if (error.message.includes("502")) return true;
710
- if (error.message.includes("503")) return true;
711
- if (error.message.includes("504")) return true;
712
- if ("retryable" in error && error.retryable === true) return true;
713
- return false;
714
- }
715
- async function retry(operation, config = {}) {
716
- const fullConfig = { ...DEFAULT_CONFIG, ...config };
717
- const state = {
718
- attempt: 0,
719
- totalDelay: 0,
720
- errors: []
721
- };
722
- while (state.attempt < fullConfig.maxAttempts) {
723
- try {
724
- return await operation();
725
- } catch (error) {
726
- state.errors.push(error instanceof Error ? error : new Error(String(error)));
727
- state.attempt++;
728
- const shouldRetry = fullConfig.retryIf?.(error) ?? isRetryableError(error);
729
- if (!shouldRetry || state.attempt >= fullConfig.maxAttempts) {
730
- throw new RetryExhaustedError(
731
- `Operation failed after ${state.attempt} attempts`,
732
- state.errors
733
- );
734
- }
735
- const delay = calculateDelay(state.attempt - 1, fullConfig);
736
- state.totalDelay += delay;
737
- fullConfig.onRetry?.(error, state.attempt, delay);
738
- await sleep(delay);
739
- }
740
- }
741
- throw new RetryExhaustedError(
742
- `Operation failed after ${state.attempt} attempts`,
743
- state.errors
744
- );
745
- }
746
- var RetryExhaustedError = class extends Error {
747
- attempts;
748
- errors;
749
- constructor(message, errors) {
750
- super(message);
751
- this.name = "RetryExhaustedError";
752
- this.attempts = errors.length;
753
- this.errors = errors;
754
- }
755
- /**
756
- * Get the last error
757
- */
758
- get lastError() {
759
- return this.errors[this.errors.length - 1];
760
- }
761
- /**
762
- * Get the first error
763
- */
764
- get firstError() {
765
- return this.errors[0];
766
- }
767
- };
768
- var DEFAULT_CIRCUIT_CONFIG = {
769
- failureThreshold: 5,
770
- resetTimeout: 3e4,
771
- successThreshold: 3,
772
- monitorWindow: 6e4
773
- };
774
- var CircuitBreaker = class {
775
- state = "closed";
776
- failures = [];
777
- successes = 0;
778
- lastFailure;
779
- config;
780
- constructor(config = {}) {
781
- this.config = { ...DEFAULT_CIRCUIT_CONFIG, ...config };
782
- }
783
- /**
784
- * Execute operation through circuit breaker
785
- */
786
- async execute(operation) {
787
- if (this.state === "open") {
788
- if (this.shouldAttemptReset()) {
789
- this.state = "half-open";
790
- } else {
791
- throw new CircuitOpenError("Circuit is open, request rejected");
792
- }
793
- }
794
- try {
795
- const result = await operation();
796
- this.onSuccess();
797
- return result;
798
- } catch (error) {
799
- this.onFailure();
800
- throw error;
801
- }
802
- }
803
- /**
804
- * Execute with Result type
805
- */
806
- async executeWithResult(operation) {
807
- try {
808
- const result = await this.execute(operation);
809
- return ok(result);
810
- } catch (error) {
811
- return err(error);
812
- }
813
- }
814
- onSuccess() {
815
- if (this.state === "half-open") {
816
- this.successes++;
817
- if (this.successes >= this.config.successThreshold) {
818
- this.reset();
819
- }
820
- }
821
- this.cleanOldFailures();
822
- }
823
- onFailure() {
824
- this.failures.push(/* @__PURE__ */ new Date());
825
- this.lastFailure = /* @__PURE__ */ new Date();
826
- this.successes = 0;
827
- this.cleanOldFailures();
828
- if (this.failures.length >= this.config.failureThreshold) {
829
- this.state = "open";
830
- }
831
- }
832
- shouldAttemptReset() {
833
- if (!this.lastFailure) return true;
834
- return Date.now() - this.lastFailure.getTime() >= this.config.resetTimeout;
835
- }
836
- cleanOldFailures() {
837
- const cutoff = Date.now() - this.config.monitorWindow;
838
- this.failures = this.failures.filter((f) => f.getTime() > cutoff);
839
- }
840
- reset() {
841
- this.state = "closed";
842
- this.failures = [];
843
- this.successes = 0;
844
- }
845
- /**
846
- * Get current circuit state
847
- */
848
- getState() {
849
- return this.state;
850
- }
851
- /**
852
- * Manually reset circuit
853
- */
854
- forceReset() {
855
- this.reset();
856
- }
857
- /**
858
- * Get circuit statistics
859
- */
860
- getStats() {
861
- return {
862
- state: this.state,
863
- failures: this.failures.length,
864
- successes: this.successes,
865
- lastFailure: this.lastFailure
866
- };
867
- }
868
- };
869
- var CircuitOpenError = class extends Error {
870
- constructor(message) {
871
- super(message);
872
- this.name = "CircuitOpenError";
873
- }
874
- };
875
- function createCircuitBreaker(config) {
876
- return new CircuitBreaker(config);
877
- }
878
-
879
- // src/core/errors.ts
880
- var RevenueError = class extends Error {
881
- code;
882
- retryable;
883
- metadata;
884
- constructor(message, code, options = {}) {
885
- super(message);
886
- this.name = this.constructor.name;
887
- this.code = code;
888
- this.retryable = options.retryable ?? false;
889
- this.metadata = options.metadata ?? {};
890
- Error.captureStackTrace(this, this.constructor);
891
- }
892
- toJSON() {
893
- return {
894
- name: this.name,
895
- message: this.message,
896
- code: this.code,
897
- retryable: this.retryable,
898
- metadata: this.metadata
899
- };
900
- }
901
- };
902
- var ConfigurationError = class extends RevenueError {
903
- constructor(message, metadata = {}) {
904
- super(message, "CONFIGURATION_ERROR", { retryable: false, metadata });
905
- }
906
- };
907
- var ModelNotRegisteredError = class extends ConfigurationError {
908
- constructor(modelName) {
909
- super(
910
- `Model "${modelName}" is not registered. Register it via createRevenue({ models: { ${modelName}: ... } })`,
911
- { modelName }
912
- );
913
- }
914
- };
915
- var ProviderError = class extends RevenueError {
916
- constructor(message, code, options = {}) {
917
- super(message, code, options);
918
- }
919
- };
920
- var ProviderNotFoundError = class extends ProviderError {
921
- constructor(providerName, availableProviders = []) {
922
- super(
923
- `Payment provider "${providerName}" not found. Available: ${availableProviders.join(", ")}`,
924
- "PROVIDER_NOT_FOUND",
925
- { retryable: false, metadata: { providerName, availableProviders } }
926
- );
927
- }
928
- };
929
- var ProviderCapabilityError = class extends ProviderError {
930
- constructor(providerName, capability) {
931
- super(
932
- `Provider "${providerName}" does not support ${capability}`,
933
- "PROVIDER_CAPABILITY_NOT_SUPPORTED",
934
- { retryable: false, metadata: { providerName, capability } }
935
- );
936
- }
937
- };
938
- var PaymentIntentCreationError = class extends ProviderError {
939
- constructor(providerName, originalError) {
940
- super(
941
- `Failed to create payment intent with provider "${providerName}": ${originalError.message}`,
942
- "PAYMENT_INTENT_CREATION_FAILED",
943
- { retryable: true, metadata: { providerName, originalError: originalError.message } }
944
- );
945
- }
946
- };
947
- var PaymentVerificationError = class extends ProviderError {
948
- constructor(paymentIntentId, reason) {
949
- super(
950
- `Payment verification failed for intent "${paymentIntentId}": ${reason}`,
951
- "PAYMENT_VERIFICATION_FAILED",
952
- { retryable: true, metadata: { paymentIntentId, reason } }
953
- );
954
- }
955
- };
956
- var NotFoundError = class extends RevenueError {
957
- constructor(message, code, metadata = {}) {
958
- super(message, code, { retryable: false, metadata });
959
- }
960
- };
961
- var SubscriptionNotFoundError = class extends NotFoundError {
962
- constructor(subscriptionId) {
963
- super(
964
- `Subscription not found: ${subscriptionId}`,
965
- "SUBSCRIPTION_NOT_FOUND",
966
- { subscriptionId }
967
- );
968
- }
969
- };
970
- var TransactionNotFoundError = class extends NotFoundError {
971
- constructor(transactionId) {
972
- super(
973
- `Transaction not found: ${transactionId}`,
974
- "TRANSACTION_NOT_FOUND",
975
- { transactionId }
976
- );
977
- }
978
- };
979
- var ValidationError = class extends RevenueError {
980
- constructor(message, metadata = {}) {
981
- super(message, "VALIDATION_ERROR", { retryable: false, metadata });
982
- }
983
- };
984
- var InvalidAmountError = class extends ValidationError {
985
- constructor(amount, message) {
986
- super(
987
- message ?? `Invalid amount: ${amount}. Amount must be non-negative`,
988
- { amount }
989
- );
990
- }
991
- };
992
- var MissingRequiredFieldError = class extends ValidationError {
993
- constructor(fieldName) {
994
- super(`Missing required field: ${fieldName}`, { fieldName });
995
- }
996
- };
997
- var StateError = class extends RevenueError {
998
- constructor(message, code, metadata = {}) {
999
- super(message, code, { retryable: false, metadata });
1000
- }
1001
- };
1002
- var AlreadyVerifiedError = class extends StateError {
1003
- constructor(transactionId) {
1004
- super(
1005
- `Transaction ${transactionId} is already verified`,
1006
- "ALREADY_VERIFIED",
1007
- { transactionId }
1008
- );
1009
- }
1010
- };
1011
- var InvalidStateTransitionError = class extends StateError {
1012
- constructor(resourceType, resourceId, fromState, toState) {
1013
- super(
1014
- `Invalid state transition for ${resourceType} ${resourceId}: ${fromState} \u2192 ${toState}`,
1015
- "INVALID_STATE_TRANSITION",
1016
- { resourceType, resourceId, fromState, toState }
1017
- );
1018
- }
1019
- };
1020
- var SubscriptionNotActiveError = class extends StateError {
1021
- constructor(subscriptionId, message) {
1022
- super(
1023
- message ?? `Subscription ${subscriptionId} is not active`,
1024
- "SUBSCRIPTION_NOT_ACTIVE",
1025
- { subscriptionId }
1026
- );
1027
- }
1028
- };
1029
- var OperationError = class extends RevenueError {
1030
- constructor(message, code, options = {}) {
1031
- super(message, code, options);
1032
- }
1033
- };
1034
- var RefundNotSupportedError = class extends OperationError {
1035
- constructor(providerName) {
1036
- super(
1037
- `Refunds are not supported by provider "${providerName}"`,
1038
- "REFUND_NOT_SUPPORTED",
1039
- { retryable: false, metadata: { providerName } }
1040
- );
1041
- }
1042
- };
1043
- var RefundError = class extends OperationError {
1044
- constructor(transactionId, reason) {
1045
- super(
1046
- `Refund failed for transaction ${transactionId}: ${reason}`,
1047
- "REFUND_FAILED",
1048
- { retryable: true, metadata: { transactionId, reason } }
1049
- );
1050
- }
1051
- };
1052
- var ERROR_CODES = {
1053
- // Configuration
1054
- CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
1055
- MODEL_NOT_REGISTERED: "MODEL_NOT_REGISTERED",
1056
- // Provider
1057
- PROVIDER_NOT_FOUND: "PROVIDER_NOT_FOUND",
1058
- PROVIDER_CAPABILITY_NOT_SUPPORTED: "PROVIDER_CAPABILITY_NOT_SUPPORTED",
1059
- PAYMENT_INTENT_CREATION_FAILED: "PAYMENT_INTENT_CREATION_FAILED",
1060
- PAYMENT_VERIFICATION_FAILED: "PAYMENT_VERIFICATION_FAILED",
1061
- // Not Found
1062
- SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
1063
- TRANSACTION_NOT_FOUND: "TRANSACTION_NOT_FOUND",
1064
- // Validation
1065
- VALIDATION_ERROR: "VALIDATION_ERROR",
1066
- INVALID_AMOUNT: "INVALID_AMOUNT",
1067
- MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
1068
- // State
1069
- ALREADY_VERIFIED: "ALREADY_VERIFIED",
1070
- INVALID_STATE_TRANSITION: "INVALID_STATE_TRANSITION",
1071
- SUBSCRIPTION_NOT_ACTIVE: "SUBSCRIPTION_NOT_ACTIVE",
1072
- // Operations
1073
- REFUND_NOT_SUPPORTED: "REFUND_NOT_SUPPORTED",
1074
- REFUND_FAILED: "REFUND_FAILED"
1075
- };
1076
- function isRetryable(error) {
1077
- return error instanceof RevenueError && error.retryable;
1078
- }
1079
- function isRevenueError(error) {
1080
- return error instanceof RevenueError;
1081
- }
1082
-
1083
- // src/enums/transaction.enums.ts
1084
- var TRANSACTION_FLOW = {
1085
- INFLOW: "inflow",
1086
- OUTFLOW: "outflow"
1087
- };
1088
- var TRANSACTION_FLOW_VALUES = Object.values(
1089
- TRANSACTION_FLOW
1090
- );
1091
- var TRANSACTION_STATUS = {
1092
- PENDING: "pending",
1093
- PAYMENT_INITIATED: "payment_initiated",
1094
- PROCESSING: "processing",
1095
- REQUIRES_ACTION: "requires_action",
1096
- VERIFIED: "verified",
1097
- COMPLETED: "completed",
1098
- FAILED: "failed",
1099
- CANCELLED: "cancelled",
1100
- EXPIRED: "expired",
1101
- REFUNDED: "refunded",
1102
- PARTIALLY_REFUNDED: "partially_refunded"
1103
- };
1104
- var TRANSACTION_STATUS_VALUES = Object.values(
1105
- TRANSACTION_STATUS
1106
- );
1107
- var LIBRARY_CATEGORIES = {
1108
- SUBSCRIPTION: "subscription",
1109
- PURCHASE: "purchase"
1110
- };
1111
- var LIBRARY_CATEGORY_VALUES = Object.values(
1112
- LIBRARY_CATEGORIES
1113
- );
1114
- new Set(TRANSACTION_FLOW_VALUES);
1115
- new Set(
1116
- TRANSACTION_STATUS_VALUES
1117
- );
1118
- new Set(LIBRARY_CATEGORY_VALUES);
1119
-
1120
- // src/shared/utils/validators/category-resolver.ts
1121
- function resolveCategory(entity, monetizationType, categoryMappings = {}) {
1122
- if (entity && categoryMappings[entity]) {
1123
- return categoryMappings[entity];
1124
- }
1125
- switch (monetizationType) {
1126
- case "subscription":
1127
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
1128
- // 'subscription'
1129
- case "purchase":
1130
- return LIBRARY_CATEGORIES.PURCHASE;
1131
- // 'purchase'
1132
- default:
1133
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
1134
- }
1135
- }
1136
-
1137
- // src/shared/utils/calculators/commission.ts
1138
- function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
1139
- if (!commissionRate || commissionRate <= 0) {
1140
- return null;
1141
- }
1142
- if (amount < 0) {
1143
- throw new Error("Transaction amount cannot be negative");
1144
- }
1145
- if (commissionRate < 0 || commissionRate > 1) {
1146
- throw new Error("Commission rate must be between 0 and 1");
1147
- }
1148
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
1149
- throw new Error("Gateway fee rate must be between 0 and 1");
1150
- }
1151
- const grossAmount = Math.round(amount * commissionRate);
1152
- const gatewayFeeAmount = Math.round(amount * gatewayFeeRate);
1153
- const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
1154
- return {
1155
- rate: commissionRate,
1156
- grossAmount,
1157
- gatewayFeeRate,
1158
- gatewayFeeAmount,
1159
- netAmount,
1160
- status: "pending"
1161
- };
1162
- }
1163
- function reverseCommission(originalCommission, originalAmount, refundAmount) {
1164
- if (!originalCommission?.netAmount) {
1165
- return null;
1166
- }
1167
- if (!originalAmount || originalAmount <= 0) {
1168
- throw new ValidationError("Original amount must be greater than 0", { originalAmount });
1169
- }
1170
- if (refundAmount < 0) {
1171
- throw new ValidationError("Refund amount cannot be negative", { refundAmount });
1172
- }
1173
- if (refundAmount > originalAmount) {
1174
- throw new ValidationError(
1175
- `Refund amount (${refundAmount}) exceeds original amount (${originalAmount})`,
1176
- { refundAmount, originalAmount }
1177
- );
1178
- }
1179
- const refundRatio = refundAmount / originalAmount;
1180
- const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio);
1181
- const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio);
1182
- const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio);
1183
- return {
1184
- rate: originalCommission.rate,
1185
- grossAmount: reversedGrossAmount,
1186
- gatewayFeeRate: originalCommission.gatewayFeeRate,
1187
- gatewayFeeAmount: reversedGatewayFee,
1188
- netAmount: reversedNetAmount,
1189
- status: "waived"
1190
- // Commission waived due to refund
1191
- };
1192
- }
1193
-
1194
- // src/infrastructure/config/resolver.ts
1195
- function resolveConfig(options) {
1196
- const config = {
1197
- targetModels: [],
1198
- categoryMappings: {}
1199
- };
1200
- if (options.commissionRate !== void 0) {
1201
- config.commissionRates = {
1202
- "*": options.commissionRate
1203
- // Global default for all categories
1204
- };
1205
- }
1206
- if (options.gatewayFeeRate !== void 0) {
1207
- config.gatewayFeeRates = {
1208
- "*": options.gatewayFeeRate
1209
- // Global default for all gateways
1210
- };
1211
- }
1212
- return config;
1213
- }
1214
- function getCommissionRate(config, category) {
1215
- if (!config?.commissionRates) return 0;
1216
- if (category in config.commissionRates) {
1217
- return config.commissionRates[category];
1218
- }
1219
- return config.commissionRates["*"] ?? 0;
1220
- }
1221
- function getGatewayFeeRate(config, gateway) {
1222
- if (!config?.gatewayFeeRates) return 0;
1223
- if (gateway in config.gatewayFeeRates) {
1224
- return config.gatewayFeeRates[gateway];
1225
- }
1226
- return config.gatewayFeeRates["*"] ?? 0;
1227
- }
1228
-
1229
- // src/enums/monetization.enums.ts
1230
- var MONETIZATION_TYPES = {
1231
- FREE: "free",
1232
- PURCHASE: "purchase",
1233
- SUBSCRIPTION: "subscription"
1234
- };
1235
- var MONETIZATION_TYPE_VALUES = Object.values(
1236
- MONETIZATION_TYPES
1237
- );
1238
- new Set(MONETIZATION_TYPE_VALUES);
1239
-
1240
- // src/core/state-machine/StateMachine.ts
1241
- var StateMachine = class {
1242
- /**
1243
- * @param transitions - Map of state → allowed next states
1244
- * @param resourceType - Type of resource (for error messages)
1245
- */
1246
- constructor(transitions, resourceType) {
1247
- this.transitions = transitions;
1248
- this.resourceType = resourceType;
1249
- }
1250
- /**
1251
- * Validate state transition is allowed
1252
- *
1253
- * @param from - Current state
1254
- * @param to - Target state
1255
- * @param resourceId - ID of the resource being transitioned
1256
- * @throws InvalidStateTransitionError if transition is invalid
1257
- *
1258
- * @example
1259
- * ```typescript
1260
- * try {
1261
- * stateMachine.validate('pending', 'completed', 'tx_123');
1262
- * } catch (error) {
1263
- * if (error instanceof InvalidStateTransitionError) {
1264
- * console.error('Invalid transition:', error.message);
1265
- * }
1266
- * }
1267
- * ```
1268
- */
1269
- validate(from, to, resourceId) {
1270
- const allowedTransitions = this.transitions.get(from);
1271
- if (!allowedTransitions?.has(to)) {
1272
- throw new InvalidStateTransitionError(
1273
- this.resourceType,
1274
- resourceId,
1275
- from,
1276
- to
1277
- );
1278
- }
1279
- }
1280
- /**
1281
- * Check if transition is valid (non-throwing)
1282
- *
1283
- * @param from - Current state
1284
- * @param to - Target state
1285
- * @returns true if transition is allowed
1286
- *
1287
- * @example
1288
- * ```typescript
1289
- * if (stateMachine.canTransition('pending', 'processing')) {
1290
- * // Safe to proceed with transition
1291
- * transaction.status = 'processing';
1292
- * }
1293
- * ```
1294
- */
1295
- canTransition(from, to) {
1296
- return this.transitions.get(from)?.has(to) ?? false;
1297
- }
1298
- /**
1299
- * Get all allowed next states from current state
1300
- *
1301
- * @param from - Current state
1302
- * @returns Array of allowed next states
1303
- *
1304
- * @example
1305
- * ```typescript
1306
- * const nextStates = stateMachine.getAllowedTransitions('pending');
1307
- * console.log(nextStates); // ['processing', 'failed']
1308
- * ```
1309
- */
1310
- getAllowedTransitions(from) {
1311
- return Array.from(this.transitions.get(from) ?? []);
1312
- }
1313
- /**
1314
- * Check if state is terminal (no outgoing transitions)
1315
- *
1316
- * @param state - State to check
1317
- * @returns true if state has no outgoing transitions
1318
- *
1319
- * @example
1320
- * ```typescript
1321
- * stateMachine.isTerminalState('completed'); // true
1322
- * stateMachine.isTerminalState('pending'); // false
1323
- * ```
1324
- */
1325
- isTerminalState(state) {
1326
- const transitions = this.transitions.get(state);
1327
- return !transitions || transitions.size === 0;
1328
- }
1329
- /**
1330
- * Get the resource type this state machine manages
1331
- *
1332
- * @returns Resource type string
1333
- */
1334
- getResourceType() {
1335
- return this.resourceType;
1336
- }
1337
- /**
1338
- * Validate state transition and create audit event
1339
- *
1340
- * This is a convenience method that combines validation with audit event creation.
1341
- * Use this when you want to both validate a transition and record it in the audit trail.
1342
- *
1343
- * @param from - Current state
1344
- * @param to - Target state
1345
- * @param resourceId - ID of the resource being transitioned
1346
- * @param context - Optional audit context (who, why, metadata)
1347
- * @returns StateChangeEvent ready to be appended to document metadata
1348
- * @throws InvalidStateTransitionError if transition is invalid
1349
- *
1350
- * @example
1351
- * ```typescript
1352
- * import { appendAuditEvent } from '@classytic/revenue';
1353
- *
1354
- * // Validate and create audit event
1355
- * const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
1356
- * transaction.status,
1357
- * 'verified',
1358
- * transaction._id.toString(),
1359
- * {
1360
- * changedBy: 'admin_123',
1361
- * reason: 'Payment verified by payment gateway',
1362
- * metadata: { verificationId: 'ver_abc' }
1363
- * }
1364
- * );
1365
- *
1366
- * // Apply state change
1367
- * transaction.status = 'verified';
1368
- *
1369
- * // Append audit event to metadata
1370
- * Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1371
- *
1372
- * // Save
1373
- * await transaction.save();
1374
- * ```
1375
- */
1376
- validateAndCreateAuditEvent(from, to, resourceId, context) {
1377
- this.validate(from, to, resourceId);
1378
- return {
1379
- resourceType: this.resourceType,
1380
- resourceId,
1381
- fromState: from,
1382
- toState: to,
1383
- changedAt: /* @__PURE__ */ new Date(),
1384
- changedBy: context?.changedBy,
1385
- reason: context?.reason,
1386
- metadata: context?.metadata
1387
- };
1388
- }
1389
- };
1390
-
1391
- // src/enums/subscription.enums.ts
1392
- var SUBSCRIPTION_STATUS = {
1393
- ACTIVE: "active",
1394
- PAUSED: "paused",
1395
- CANCELLED: "cancelled",
1396
- EXPIRED: "expired",
1397
- PENDING: "pending",
1398
- PENDING_RENEWAL: "pending_renewal",
1399
- INACTIVE: "inactive"
1400
- };
1401
- var SUBSCRIPTION_STATUS_VALUES = Object.values(
1402
- SUBSCRIPTION_STATUS
1403
- );
1404
- var PLAN_KEYS = {
1405
- MONTHLY: "monthly",
1406
- QUARTERLY: "quarterly",
1407
- YEARLY: "yearly"
1408
- };
1409
- var PLAN_KEY_VALUES = Object.values(PLAN_KEYS);
1410
- new Set(
1411
- SUBSCRIPTION_STATUS_VALUES
1412
- );
1413
- new Set(PLAN_KEY_VALUES);
1414
-
1415
- // src/enums/settlement.enums.ts
1416
- var SETTLEMENT_STATUS = {
1417
- PENDING: "pending",
1418
- PROCESSING: "processing",
1419
- COMPLETED: "completed",
1420
- FAILED: "failed",
1421
- CANCELLED: "cancelled"
1422
- };
1423
- var SETTLEMENT_TYPE = {
1424
- SPLIT_PAYOUT: "split_payout"};
1425
-
1426
- // src/enums/escrow.enums.ts
1427
- var HOLD_STATUS = {
1428
- PENDING: "pending",
1429
- HELD: "held",
1430
- RELEASED: "released",
1431
- CANCELLED: "cancelled",
1432
- EXPIRED: "expired",
1433
- PARTIALLY_RELEASED: "partially_released"
1434
- };
1435
- var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
1436
- var RELEASE_REASON = {
1437
- PAYMENT_VERIFIED: "payment_verified",
1438
- MANUAL_RELEASE: "manual_release",
1439
- AUTO_RELEASE: "auto_release",
1440
- DISPUTE_RESOLVED: "dispute_resolved"
1441
- };
1442
- var RELEASE_REASON_VALUES = Object.values(
1443
- RELEASE_REASON
1444
- );
1445
- var HOLD_REASON = {
1446
- PAYMENT_VERIFICATION: "payment_verification",
1447
- FRAUD_CHECK: "fraud_check",
1448
- MANUAL_REVIEW: "manual_review",
1449
- DISPUTE: "dispute",
1450
- COMPLIANCE: "compliance"
1451
- };
1452
- var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
1453
- new Set(HOLD_STATUS_VALUES);
1454
- new Set(RELEASE_REASON_VALUES);
1455
- new Set(HOLD_REASON_VALUES);
1456
-
1457
- // src/enums/split.enums.ts
1458
- var SPLIT_TYPE = {
1459
- PLATFORM_COMMISSION: "platform_commission",
1460
- AFFILIATE_COMMISSION: "affiliate_commission",
1461
- REFERRAL_COMMISSION: "referral_commission",
1462
- PARTNER_COMMISSION: "partner_commission",
1463
- CUSTOM: "custom"
1464
- };
1465
- var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
1466
- var SPLIT_STATUS = {
1467
- PENDING: "pending",
1468
- DUE: "due",
1469
- PAID: "paid",
1470
- WAIVED: "waived",
1471
- CANCELLED: "cancelled"
1472
- };
1473
- var SPLIT_STATUS_VALUES = Object.values(
1474
- SPLIT_STATUS
1475
- );
1476
- var PAYOUT_METHOD = {
1477
- BANK_TRANSFER: "bank_transfer",
1478
- MOBILE_WALLET: "mobile_wallet",
1479
- PLATFORM_BALANCE: "platform_balance",
1480
- CRYPTO: "crypto",
1481
- CHECK: "check",
1482
- MANUAL: "manual"
1483
- };
1484
- var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
1485
- new Set(SPLIT_TYPE_VALUES);
1486
- new Set(SPLIT_STATUS_VALUES);
1487
- new Set(PAYOUT_METHOD_VALUES);
1488
-
1489
- // src/core/state-machine/definitions.ts
1490
- var TRANSACTION_STATE_MACHINE = new StateMachine(
1491
- /* @__PURE__ */ new Map([
1492
- [
1493
- TRANSACTION_STATUS.PENDING,
1494
- /* @__PURE__ */ new Set([
1495
- TRANSACTION_STATUS.PAYMENT_INITIATED,
1496
- TRANSACTION_STATUS.PROCESSING,
1497
- TRANSACTION_STATUS.VERIFIED,
1498
- // Allow direct verification (manual payments)
1499
- TRANSACTION_STATUS.FAILED,
1500
- TRANSACTION_STATUS.CANCELLED
1501
- ])
1502
- ],
1503
- [
1504
- TRANSACTION_STATUS.PAYMENT_INITIATED,
1505
- /* @__PURE__ */ new Set([
1506
- TRANSACTION_STATUS.PROCESSING,
1507
- TRANSACTION_STATUS.VERIFIED,
1508
- // Allow direct verification (webhook/instant payments)
1509
- TRANSACTION_STATUS.REQUIRES_ACTION,
1510
- TRANSACTION_STATUS.FAILED,
1511
- TRANSACTION_STATUS.CANCELLED
1512
- ])
1513
- ],
1514
- [
1515
- TRANSACTION_STATUS.PROCESSING,
1516
- /* @__PURE__ */ new Set([
1517
- TRANSACTION_STATUS.VERIFIED,
1518
- TRANSACTION_STATUS.REQUIRES_ACTION,
1519
- TRANSACTION_STATUS.FAILED
1520
- ])
1521
- ],
1522
- [
1523
- TRANSACTION_STATUS.REQUIRES_ACTION,
1524
- /* @__PURE__ */ new Set([
1525
- TRANSACTION_STATUS.VERIFIED,
1526
- TRANSACTION_STATUS.FAILED,
1527
- TRANSACTION_STATUS.CANCELLED,
1528
- TRANSACTION_STATUS.EXPIRED
1529
- ])
1530
- ],
1531
- [
1532
- TRANSACTION_STATUS.VERIFIED,
1533
- /* @__PURE__ */ new Set([
1534
- TRANSACTION_STATUS.COMPLETED,
1535
- TRANSACTION_STATUS.REFUNDED,
1536
- TRANSACTION_STATUS.PARTIALLY_REFUNDED,
1537
- TRANSACTION_STATUS.CANCELLED
1538
- ])
1539
- ],
1540
- [
1541
- TRANSACTION_STATUS.COMPLETED,
1542
- /* @__PURE__ */ new Set([
1543
- TRANSACTION_STATUS.REFUNDED,
1544
- TRANSACTION_STATUS.PARTIALLY_REFUNDED
1545
- ])
1546
- ],
1547
- [
1548
- TRANSACTION_STATUS.PARTIALLY_REFUNDED,
1549
- /* @__PURE__ */ new Set([TRANSACTION_STATUS.REFUNDED])
1550
- ],
1551
- // Terminal states
1552
- [TRANSACTION_STATUS.FAILED, /* @__PURE__ */ new Set([])],
1553
- [TRANSACTION_STATUS.REFUNDED, /* @__PURE__ */ new Set([])],
1554
- [TRANSACTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
1555
- [TRANSACTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
1556
- ]),
1557
- "transaction"
1558
- );
1559
- var SUBSCRIPTION_STATE_MACHINE = new StateMachine(
1560
- /* @__PURE__ */ new Map([
1561
- [
1562
- SUBSCRIPTION_STATUS.PENDING,
1563
- /* @__PURE__ */ new Set([
1564
- SUBSCRIPTION_STATUS.ACTIVE,
1565
- SUBSCRIPTION_STATUS.CANCELLED
1566
- ])
1567
- ],
1568
- [
1569
- SUBSCRIPTION_STATUS.ACTIVE,
1570
- /* @__PURE__ */ new Set([
1571
- SUBSCRIPTION_STATUS.PAUSED,
1572
- SUBSCRIPTION_STATUS.PENDING_RENEWAL,
1573
- SUBSCRIPTION_STATUS.CANCELLED,
1574
- SUBSCRIPTION_STATUS.EXPIRED
1575
- ])
1576
- ],
1577
- [
1578
- SUBSCRIPTION_STATUS.PENDING_RENEWAL,
1579
- /* @__PURE__ */ new Set([
1580
- SUBSCRIPTION_STATUS.ACTIVE,
1581
- SUBSCRIPTION_STATUS.CANCELLED,
1582
- SUBSCRIPTION_STATUS.EXPIRED
1583
- ])
1584
- ],
1585
- [
1586
- SUBSCRIPTION_STATUS.PAUSED,
1587
- /* @__PURE__ */ new Set([
1588
- SUBSCRIPTION_STATUS.ACTIVE,
1589
- SUBSCRIPTION_STATUS.CANCELLED
1590
- ])
1591
- ],
1592
- [
1593
- SUBSCRIPTION_STATUS.INACTIVE,
1594
- /* @__PURE__ */ new Set([
1595
- SUBSCRIPTION_STATUS.ACTIVE,
1596
- SUBSCRIPTION_STATUS.CANCELLED
1597
- ])
1598
- ],
1599
- // Terminal states
1600
- [SUBSCRIPTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
1601
- [SUBSCRIPTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
1602
- ]),
1603
- "subscription"
1604
- );
1605
- var SETTLEMENT_STATE_MACHINE = new StateMachine(
1606
- /* @__PURE__ */ new Map([
1607
- [
1608
- SETTLEMENT_STATUS.PENDING,
1609
- /* @__PURE__ */ new Set([
1610
- SETTLEMENT_STATUS.PROCESSING,
1611
- SETTLEMENT_STATUS.CANCELLED
1612
- ])
1613
- ],
1614
- [
1615
- SETTLEMENT_STATUS.PROCESSING,
1616
- /* @__PURE__ */ new Set([
1617
- SETTLEMENT_STATUS.COMPLETED,
1618
- SETTLEMENT_STATUS.FAILED
1619
- ])
1620
- ],
1621
- [
1622
- SETTLEMENT_STATUS.FAILED,
1623
- /* @__PURE__ */ new Set([
1624
- SETTLEMENT_STATUS.PENDING,
1625
- // Allow retry
1626
- SETTLEMENT_STATUS.CANCELLED
1627
- ])
1628
- ],
1629
- // Terminal states
1630
- [SETTLEMENT_STATUS.COMPLETED, /* @__PURE__ */ new Set([])],
1631
- [SETTLEMENT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
1632
- ]),
1633
- "settlement"
1634
- );
1635
- var HOLD_STATE_MACHINE = new StateMachine(
1636
- /* @__PURE__ */ new Map([
1637
- [
1638
- HOLD_STATUS.HELD,
1639
- /* @__PURE__ */ new Set([
1640
- HOLD_STATUS.RELEASED,
1641
- HOLD_STATUS.PARTIALLY_RELEASED,
1642
- HOLD_STATUS.CANCELLED,
1643
- HOLD_STATUS.EXPIRED
1644
- ])
1645
- ],
1646
- [
1647
- HOLD_STATUS.PARTIALLY_RELEASED,
1648
- /* @__PURE__ */ new Set([
1649
- HOLD_STATUS.RELEASED,
1650
- HOLD_STATUS.CANCELLED
1651
- ])
1652
- ],
1653
- // Terminal states
1654
- [HOLD_STATUS.RELEASED, /* @__PURE__ */ new Set([])],
1655
- [HOLD_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
1656
- [HOLD_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
1657
- ]),
1658
- "escrow_hold"
1659
- );
1660
- var SPLIT_STATE_MACHINE = new StateMachine(
1661
- /* @__PURE__ */ new Map([
1662
- [
1663
- SPLIT_STATUS.PENDING,
1664
- /* @__PURE__ */ new Set([
1665
- SPLIT_STATUS.DUE,
1666
- SPLIT_STATUS.PAID,
1667
- SPLIT_STATUS.WAIVED,
1668
- SPLIT_STATUS.CANCELLED
1669
- ])
1670
- ],
1671
- [
1672
- SPLIT_STATUS.DUE,
1673
- /* @__PURE__ */ new Set([
1674
- SPLIT_STATUS.PAID,
1675
- SPLIT_STATUS.WAIVED,
1676
- SPLIT_STATUS.CANCELLED
1677
- ])
1678
- ],
1679
- // Terminal states
1680
- [SPLIT_STATUS.PAID, /* @__PURE__ */ new Set([])],
1681
- [SPLIT_STATUS.WAIVED, /* @__PURE__ */ new Set([])],
1682
- [SPLIT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
1683
- ]),
1684
- "split"
1685
- );
1686
-
1687
- // src/infrastructure/audit/types.ts
1688
- function appendAuditEvent(document, event) {
1689
- const stateHistory = document.metadata?.stateHistory ?? [];
1690
- return {
1691
- ...document,
1692
- metadata: {
1693
- ...document.metadata,
1694
- stateHistory: [...stateHistory, event]
1695
- }
1696
- };
1697
- }
1698
-
1699
- // src/application/services/monetization.service.ts
1700
- var MonetizationService = class {
1701
- models;
1702
- providers;
1703
- config;
1704
- plugins;
1705
- logger;
1706
- events;
1707
- retryConfig;
1708
- circuitBreaker;
1709
- constructor(container) {
1710
- this.models = container.get("models");
1711
- this.providers = container.get("providers");
1712
- this.config = container.get("config");
1713
- this.plugins = container.get("plugins");
1714
- this.logger = container.get("logger");
1715
- this.events = container.get("events");
1716
- this.retryConfig = container.get("retryConfig");
1717
- this.circuitBreaker = container.get("circuitBreaker");
1718
- }
1719
- /**
1720
- * Create plugin context for hook execution
1721
- * @private
1722
- */
1723
- getPluginContext(idempotencyKey) {
1724
- return {
1725
- events: this.events,
1726
- logger: this.logger,
1727
- storage: /* @__PURE__ */ new Map(),
1728
- meta: {
1729
- idempotencyKey: idempotencyKey ?? void 0,
1730
- requestId: nanoid(),
1731
- timestamp: /* @__PURE__ */ new Date()
1732
- }
1733
- };
1734
- }
1735
- /**
1736
- * Execute provider call with retry and circuit breaker protection
1737
- * @private
1738
- */
1739
- async executeProviderCall(operation, operationName) {
1740
- const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
1741
- if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
1742
- return retry(withCircuitBreaker, {
1743
- ...this.retryConfig,
1744
- onRetry: (error, attempt, delay) => {
1745
- this.logger.warn(
1746
- `[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
1747
- error
1748
- );
1749
- this.retryConfig.onRetry?.(error, attempt, delay);
1750
- }
1751
- });
1752
- }
1753
- return withCircuitBreaker();
1754
- }
1755
- /**
1756
- * Create a new monetization (purchase, subscription, or free item)
1757
- *
1758
- * @param params - Monetization parameters
1759
- *
1760
- * @example
1761
- * // One-time purchase
1762
- * await revenue.monetization.create({
1763
- * data: {
1764
- * organizationId: '...',
1765
- * customerId: '...',
1766
- * sourceId: order._id,
1767
- * sourceModel: 'Order',
1768
- * },
1769
- * planKey: 'one_time',
1770
- * monetizationType: 'purchase',
1771
- * gateway: 'bkash',
1772
- * amount: 1500,
1773
- * });
1774
- *
1775
- * // Recurring subscription
1776
- * await revenue.monetization.create({
1777
- * data: {
1778
- * organizationId: '...',
1779
- * customerId: '...',
1780
- * sourceId: subscription._id,
1781
- * sourceModel: 'Subscription',
1782
- * },
1783
- * planKey: 'monthly',
1784
- * monetizationType: 'subscription',
1785
- * gateway: 'stripe',
1786
- * amount: 2000,
1787
- * });
1788
- *
1789
- * @returns Result with subscription, transaction, and paymentIntent
1790
- */
1791
- async create(params) {
1792
- return this.plugins.executeHook(
1793
- "monetization.create.before",
1794
- this.getPluginContext(params.idempotencyKey),
1795
- params,
1796
- async () => {
1797
- const {
1798
- data,
1799
- planKey,
1800
- amount,
1801
- currency = "BDT",
1802
- gateway = "manual",
1803
- entity = null,
1804
- monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
1805
- paymentData,
1806
- metadata = {},
1807
- idempotencyKey = null
1808
- } = params;
1809
- if (!planKey) {
1810
- throw new MissingRequiredFieldError("planKey");
1811
- }
1812
- if (amount < 0) {
1813
- throw new InvalidAmountError(amount);
1814
- }
1815
- const isFree = amount === 0;
1816
- const provider = this.providers[gateway];
1817
- if (!provider) {
1818
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1819
- }
1820
- let paymentIntent = null;
1821
- let transaction = null;
1822
- if (!isFree) {
1823
- try {
1824
- paymentIntent = await this.executeProviderCall(
1825
- () => provider.createIntent({
1826
- amount,
1827
- currency,
1828
- metadata: {
1829
- ...metadata,
1830
- type: "subscription",
1831
- planKey
1832
- }
1833
- }),
1834
- `${gateway}.createIntent`
1835
- );
1836
- } catch (error) {
1837
- throw new PaymentIntentCreationError(gateway, error);
1838
- }
1839
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
1840
- const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_FLOW.INFLOW;
1841
- const commissionRate = getCommissionRate(this.config, category);
1842
- const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
1843
- const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
1844
- const tax = params.tax;
1845
- const TransactionModel = this.models.Transaction;
1846
- const baseAmount = tax?.pricesIncludeTax ? tax.baseAmount : amount;
1847
- const feeAmount = commission?.gatewayFeeAmount || 0;
1848
- const taxAmount = tax?.taxAmount || 0;
1849
- const netAmount = baseAmount - feeAmount - taxAmount;
1850
- transaction = await TransactionModel.create({
1851
- organizationId: data.organizationId,
1852
- customerId: data.customerId ?? null,
1853
- // ✅ UNIFIED: Use category as type directly
1854
- type: category,
1855
- // 'subscription', 'purchase', etc.
1856
- flow: transactionFlow,
1857
- // ✅ Use config-driven transaction type
1858
- // Auto-tagging (middleware will handle, but we can set explicitly)
1859
- tags: category === "subscription" ? ["recurring", "subscription"] : [],
1860
- // ✅ UNIFIED: Amount structure
1861
- // When prices include tax, use baseAmount (tax already extracted)
1862
- amount: baseAmount,
1863
- currency,
1864
- fee: feeAmount,
1865
- tax: taxAmount,
1866
- net: netAmount,
1867
- // ✅ UNIFIED: Tax details (if tax plugin used)
1868
- ...tax && {
1869
- taxDetails: {
1870
- type: tax.type === "collected" ? "sales_tax" : tax.type === "paid" ? "vat" : "none",
1871
- rate: tax.rate || 0,
1872
- isInclusive: tax.pricesIncludeTax || false
1873
- }
1874
- },
1875
- method: paymentData?.method ?? "manual",
1876
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1877
- // ✅ Map 'succeeded' to valid TransactionStatusValue
1878
- gateway: {
1879
- type: gateway,
1880
- // Gateway/provider type (e.g., 'stripe', 'manual')
1881
- provider: gateway,
1882
- sessionId: paymentIntent.sessionId,
1883
- paymentIntentId: paymentIntent.paymentIntentId,
1884
- chargeId: paymentIntent.id,
1885
- metadata: paymentIntent.metadata
1886
- },
1887
- paymentDetails: {
1888
- ...paymentData
1889
- },
1890
- // Commission (for marketplace/splits)
1891
- ...commission && { commission },
1892
- // ✅ UNIFIED: Source reference (renamed from reference)
1893
- ...data.sourceId && { sourceId: data.sourceId },
1894
- ...data.sourceModel && { sourceModel: data.sourceModel },
1895
- metadata: {
1896
- ...metadata,
1897
- planKey,
1898
- entity,
1899
- monetizationType,
1900
- paymentIntentId: paymentIntent.id
1901
- },
1902
- idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
1903
- });
1904
- }
1905
- let subscription = null;
1906
- if (this.models.Subscription) {
1907
- const SubscriptionModel = this.models.Subscription;
1908
- const result2 = await this.plugins.executeHook(
1909
- "subscription.create.before",
1910
- this.getPluginContext(idempotencyKey),
1911
- {
1912
- subscriptionId: void 0,
1913
- // Not yet created - populated in after hook
1914
- planKey,
1915
- customerId: data.customerId,
1916
- organizationId: data.organizationId,
1917
- entity
1918
- },
1919
- async () => {
1920
- const subscriptionData = {
1921
- organizationId: data.organizationId,
1922
- customerId: data.customerId ?? null,
1923
- planKey,
1924
- amount,
1925
- currency,
1926
- status: isFree ? "active" : "pending",
1927
- isActive: isFree,
1928
- gateway,
1929
- transactionId: transaction?._id ?? null,
1930
- paymentIntentId: paymentIntent?.id ?? null,
1931
- metadata: {
1932
- ...metadata,
1933
- isFree,
1934
- entity,
1935
- monetizationType
1936
- },
1937
- ...data
1938
- };
1939
- delete subscriptionData.sourceId;
1940
- delete subscriptionData.sourceModel;
1941
- const sub = await SubscriptionModel.create(subscriptionData);
1942
- await this.plugins.executeHook(
1943
- "subscription.create.after",
1944
- this.getPluginContext(idempotencyKey),
1945
- {
1946
- subscriptionId: sub._id.toString(),
1947
- planKey,
1948
- customerId: data.customerId,
1949
- organizationId: data.organizationId,
1950
- entity
1951
- },
1952
- async () => ({ subscription: sub, transaction })
1953
- );
1954
- return { subscription: sub, transaction };
1955
- }
1956
- );
1957
- subscription = result2.subscription;
1958
- }
1959
- this.events.emit("monetization.created", {
1960
- monetizationType,
1961
- subscription: subscription ?? void 0,
1962
- transaction: transaction ?? void 0,
1963
- paymentIntent: paymentIntent ?? void 0
1964
- });
1965
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
1966
- if (transaction) {
1967
- this.events.emit("purchase.created", {
1968
- monetizationType,
1969
- subscription: subscription ?? void 0,
1970
- transaction,
1971
- paymentIntent: paymentIntent ?? void 0
1972
- });
1973
- }
1974
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
1975
- if (subscription) {
1976
- this.events.emit("subscription.created", {
1977
- subscriptionId: subscription._id.toString(),
1978
- subscription,
1979
- transactionId: transaction?._id?.toString()
1980
- });
1981
- }
1982
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
1983
- this.events.emit("free.created", {
1984
- monetizationType,
1985
- subscription: subscription ?? void 0,
1986
- transaction: transaction ?? void 0,
1987
- paymentIntent: paymentIntent ?? void 0
1988
- });
1989
- }
1990
- const result = {
1991
- subscription,
1992
- transaction,
1993
- paymentIntent
1994
- };
1995
- return this.plugins.executeHook(
1996
- "monetization.create.after",
1997
- this.getPluginContext(params.idempotencyKey),
1998
- params,
1999
- async () => result
2000
- );
2001
- }
2002
- );
2003
- }
2004
- /**
2005
- * Activate subscription after payment verification
2006
- *
2007
- * @param subscriptionId - Subscription ID or transaction ID
2008
- * @param options - Activation options
2009
- * @returns Updated subscription
2010
- */
2011
- async activate(subscriptionId, options = {}) {
2012
- return this.plugins.executeHook(
2013
- "subscription.activate.before",
2014
- this.getPluginContext(),
2015
- { subscriptionId, ...options },
2016
- async () => {
2017
- const { timestamp = /* @__PURE__ */ new Date() } = options;
2018
- if (!this.models.Subscription) {
2019
- throw new ModelNotRegisteredError("Subscription");
2020
- }
2021
- const SubscriptionModel = this.models.Subscription;
2022
- const subscription = await SubscriptionModel.findById(subscriptionId);
2023
- if (!subscription) {
2024
- throw new SubscriptionNotFoundError(subscriptionId);
2025
- }
2026
- if (subscription.isActive) {
2027
- this.logger.warn("Subscription already active", { subscriptionId });
2028
- return subscription;
2029
- }
2030
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
2031
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
2032
- subscription.status,
2033
- "active",
2034
- subscription._id.toString(),
2035
- {
2036
- changedBy: "system",
2037
- reason: `Subscription activated for plan: ${subscription.planKey}`,
2038
- metadata: { planKey: subscription.planKey, startDate: timestamp, endDate: periodEnd }
2039
- }
2040
- );
2041
- subscription.isActive = true;
2042
- subscription.status = "active";
2043
- subscription.startDate = timestamp;
2044
- subscription.endDate = periodEnd;
2045
- subscription.activatedAt = timestamp;
2046
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
2047
- await subscription.save();
2048
- this.events.emit("subscription.activated", {
2049
- subscription,
2050
- activatedAt: timestamp
2051
- });
2052
- return this.plugins.executeHook(
2053
- "subscription.activate.after",
2054
- this.getPluginContext(),
2055
- { subscriptionId, ...options },
2056
- async () => subscription
2057
- );
2058
- }
2059
- );
2060
- }
2061
- /**
2062
- * Renew subscription
2063
- *
2064
- * @param subscriptionId - Subscription ID
2065
- * @param params - Renewal parameters
2066
- * @returns { subscription, transaction, paymentIntent }
2067
- */
2068
- async renew(subscriptionId, params = {}) {
2069
- const {
2070
- gateway = "manual",
2071
- entity = null,
2072
- paymentData,
2073
- metadata = {},
2074
- idempotencyKey = null
2075
- } = params;
2076
- if (!this.models.Subscription) {
2077
- throw new ModelNotRegisteredError("Subscription");
2078
- }
2079
- const SubscriptionModel = this.models.Subscription;
2080
- const subscription = await SubscriptionModel.findById(subscriptionId);
2081
- if (!subscription) {
2082
- throw new SubscriptionNotFoundError(subscriptionId);
2083
- }
2084
- if (subscription.amount === 0) {
2085
- throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
2086
- }
2087
- const provider = this.providers[gateway];
2088
- if (!provider) {
2089
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
2090
- }
2091
- let paymentIntent = null;
2092
- try {
2093
- paymentIntent = await provider.createIntent({
2094
- amount: subscription.amount,
2095
- currency: subscription.currency ?? "BDT",
2096
- metadata: {
2097
- ...metadata,
2098
- type: "subscription_renewal",
2099
- subscriptionId: subscription._id.toString()
2100
- }
2101
- });
2102
- } catch (error) {
2103
- this.logger.error("Failed to create payment intent for renewal:", error);
2104
- throw new PaymentIntentCreationError(gateway, error);
2105
- }
2106
- const effectiveEntity = entity ?? subscription.metadata?.entity;
2107
- const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
2108
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
2109
- const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_FLOW.INFLOW;
2110
- const commissionRate = getCommissionRate(this.config, category);
2111
- const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
2112
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
2113
- const feeAmount = commission?.gatewayFeeAmount || 0;
2114
- const netAmount = subscription.amount - feeAmount;
2115
- const TransactionModel = this.models.Transaction;
2116
- const transaction = await TransactionModel.create({
2117
- organizationId: subscription.organizationId,
2118
- customerId: subscription.customerId,
2119
- // ✅ UNIFIED: Use category as type directly
2120
- type: category,
2121
- // 'subscription', etc.
2122
- flow: transactionFlow,
2123
- // ✅ Use config-driven transaction type
2124
- tags: ["recurring", "subscription", "renewal"],
2125
- // ✅ UNIFIED: Amount structure
2126
- amount: subscription.amount,
2127
- currency: subscription.currency ?? "BDT",
2128
- fee: feeAmount,
2129
- tax: 0,
2130
- // Tax plugin would add this
2131
- net: netAmount,
2132
- method: paymentData?.method ?? "manual",
2133
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
2134
- // ✅ Map 'succeeded' to valid TransactionStatusValue
2135
- gateway: {
2136
- provider: gateway,
2137
- sessionId: paymentIntent.sessionId,
2138
- paymentIntentId: paymentIntent.paymentIntentId,
2139
- chargeId: paymentIntent.id,
2140
- metadata: paymentIntent.metadata
2141
- },
2142
- paymentDetails: {
2143
- provider: gateway,
2144
- ...paymentData
2145
- },
2146
- // Commission (for marketplace/splits)
2147
- ...commission && { commission },
2148
- // ✅ UNIFIED: Source reference (renamed from reference)
2149
- sourceId: subscription._id,
2150
- sourceModel: "Subscription",
2151
- metadata: {
2152
- ...metadata,
2153
- subscriptionId: subscription._id.toString(),
2154
- // Keep for backward compat
2155
- entity: effectiveEntity,
2156
- monetizationType: effectiveMonetizationType,
2157
- isRenewal: true,
2158
- paymentIntentId: paymentIntent.id
2159
- },
2160
- idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
2161
- });
2162
- subscription.status = "pending_renewal";
2163
- subscription.renewalTransactionId = transaction._id;
2164
- subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
2165
- await subscription.save();
2166
- this.events.emit("subscription.renewed", {
2167
- subscription,
2168
- transaction,
2169
- paymentIntent: paymentIntent ?? void 0,
2170
- renewalCount: subscription.renewalCount
2171
- });
2172
- return {
2173
- subscription,
2174
- transaction,
2175
- paymentIntent
2176
- };
2177
- }
2178
- /**
2179
- * Cancel subscription
2180
- *
2181
- * @param subscriptionId - Subscription ID
2182
- * @param options - Cancellation options
2183
- * @returns Updated subscription
2184
- */
2185
- async cancel(subscriptionId, options = {}) {
2186
- return this.plugins.executeHook(
2187
- "subscription.cancel.before",
2188
- this.getPluginContext(),
2189
- { subscriptionId, ...options },
2190
- async () => {
2191
- const { immediate = false, reason = null } = options;
2192
- if (!this.models.Subscription) {
2193
- throw new ModelNotRegisteredError("Subscription");
2194
- }
2195
- const SubscriptionModel = this.models.Subscription;
2196
- const subscription = await SubscriptionModel.findById(subscriptionId);
2197
- if (!subscription) {
2198
- throw new SubscriptionNotFoundError(subscriptionId);
2199
- }
2200
- const now = /* @__PURE__ */ new Date();
2201
- if (immediate) {
2202
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
2203
- subscription.status,
2204
- "cancelled",
2205
- subscription._id.toString(),
2206
- {
2207
- changedBy: "system",
2208
- reason: `Subscription cancelled immediately${reason ? ": " + reason : ""}`,
2209
- metadata: { cancellationReason: reason, immediate: true }
2210
- }
2211
- );
2212
- subscription.isActive = false;
2213
- subscription.status = "cancelled";
2214
- subscription.canceledAt = now;
2215
- subscription.cancellationReason = reason;
2216
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
2217
- } else {
2218
- subscription.cancelAt = subscription.endDate ?? now;
2219
- subscription.cancellationReason = reason;
2220
- }
2221
- await subscription.save();
2222
- this.events.emit("subscription.cancelled", {
2223
- subscription,
2224
- immediate,
2225
- reason: reason ?? void 0,
2226
- canceledAt: immediate ? now : subscription.cancelAt
2227
- });
2228
- return this.plugins.executeHook(
2229
- "subscription.cancel.after",
2230
- this.getPluginContext(),
2231
- { subscriptionId, ...options },
2232
- async () => subscription
2233
- );
2234
- }
2235
- );
2236
- }
2237
- /**
2238
- * Pause subscription
2239
- *
2240
- * @param subscriptionId - Subscription ID
2241
- * @param options - Pause options
2242
- * @returns Updated subscription
2243
- */
2244
- async pause(subscriptionId, options = {}) {
2245
- return this.plugins.executeHook(
2246
- "subscription.pause.before",
2247
- this.getPluginContext(),
2248
- { subscriptionId, ...options },
2249
- async () => {
2250
- const { reason = null } = options;
2251
- if (!this.models.Subscription) {
2252
- throw new ModelNotRegisteredError("Subscription");
2253
- }
2254
- const SubscriptionModel = this.models.Subscription;
2255
- const subscription = await SubscriptionModel.findById(subscriptionId);
2256
- if (!subscription) {
2257
- throw new SubscriptionNotFoundError(subscriptionId);
2258
- }
2259
- if (!subscription.isActive) {
2260
- throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
2261
- }
2262
- const pausedAt = /* @__PURE__ */ new Date();
2263
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
2264
- subscription.status,
2265
- "paused",
2266
- subscription._id.toString(),
2267
- {
2268
- changedBy: "system",
2269
- reason: `Subscription paused${reason ? ": " + reason : ""}`,
2270
- metadata: { pauseReason: reason, pausedAt }
2271
- }
2272
- );
2273
- subscription.isActive = false;
2274
- subscription.status = "paused";
2275
- subscription.pausedAt = pausedAt;
2276
- subscription.pauseReason = reason;
2277
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
2278
- await subscription.save();
2279
- this.events.emit("subscription.paused", {
2280
- subscription,
2281
- reason: reason ?? void 0,
2282
- pausedAt
2283
- });
2284
- return this.plugins.executeHook(
2285
- "subscription.pause.after",
2286
- this.getPluginContext(),
2287
- { subscriptionId, ...options },
2288
- async () => subscription
2289
- );
2290
- }
2291
- );
2292
- }
2293
- /**
2294
- * Resume subscription
2295
- *
2296
- * @param subscriptionId - Subscription ID
2297
- * @param options - Resume options
2298
- * @returns Updated subscription
2299
- */
2300
- async resume(subscriptionId, options = {}) {
2301
- return this.plugins.executeHook(
2302
- "subscription.resume.before",
2303
- this.getPluginContext(),
2304
- { subscriptionId, ...options },
2305
- async () => {
2306
- const { extendPeriod = false } = options;
2307
- if (!this.models.Subscription) {
2308
- throw new ModelNotRegisteredError("Subscription");
2309
- }
2310
- const SubscriptionModel = this.models.Subscription;
2311
- const subscription = await SubscriptionModel.findById(subscriptionId);
2312
- if (!subscription) {
2313
- throw new SubscriptionNotFoundError(subscriptionId);
2314
- }
2315
- if (!subscription.pausedAt) {
2316
- throw new InvalidStateTransitionError(
2317
- "resume",
2318
- "paused",
2319
- subscription.status,
2320
- "Only paused subscriptions can be resumed"
2321
- );
2322
- }
2323
- const now = /* @__PURE__ */ new Date();
2324
- const pausedAt = new Date(subscription.pausedAt);
2325
- const pauseDuration = now.getTime() - pausedAt.getTime();
2326
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
2327
- subscription.status,
2328
- "active",
2329
- subscription._id.toString(),
2330
- {
2331
- changedBy: "system",
2332
- reason: "Subscription resumed from paused state",
2333
- metadata: {
2334
- pausedAt,
2335
- pauseDuration,
2336
- extendPeriod,
2337
- newEndDate: extendPeriod && subscription.endDate ? new Date(new Date(subscription.endDate).getTime() + pauseDuration) : void 0
2338
- }
2339
- }
2340
- );
2341
- subscription.isActive = true;
2342
- subscription.status = "active";
2343
- subscription.pausedAt = null;
2344
- subscription.pauseReason = null;
2345
- if (extendPeriod && subscription.endDate) {
2346
- const currentEnd = new Date(subscription.endDate);
2347
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
2348
- }
2349
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
2350
- await subscription.save();
2351
- this.events.emit("subscription.resumed", {
2352
- subscription,
2353
- extendPeriod,
2354
- pauseDuration,
2355
- resumedAt: now
2356
- });
2357
- return this.plugins.executeHook(
2358
- "subscription.resume.after",
2359
- this.getPluginContext(),
2360
- { subscriptionId, ...options },
2361
- async () => subscription
2362
- );
2363
- }
2364
- );
2365
- }
2366
- /**
2367
- * List subscriptions with filters
2368
- *
2369
- * @param filters - Query filters
2370
- * @param options - Query options (limit, skip, sort)
2371
- * @returns Subscriptions
2372
- */
2373
- async list(filters = {}, options = {}) {
2374
- if (!this.models.Subscription) {
2375
- throw new ModelNotRegisteredError("Subscription");
2376
- }
2377
- const SubscriptionModel = this.models.Subscription;
2378
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
2379
- const subscriptions = await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
2380
- return subscriptions;
2381
- }
2382
- /**
2383
- * Get subscription by ID
2384
- *
2385
- * @param subscriptionId - Subscription ID
2386
- * @returns Subscription
2387
- */
2388
- async get(subscriptionId) {
2389
- if (!this.models.Subscription) {
2390
- throw new ModelNotRegisteredError("Subscription");
2391
- }
2392
- const SubscriptionModel = this.models.Subscription;
2393
- const subscription = await SubscriptionModel.findById(subscriptionId);
2394
- if (!subscription) {
2395
- throw new SubscriptionNotFoundError(subscriptionId);
2396
- }
2397
- return subscription;
2398
- }
2399
- /**
2400
- * Calculate period end date based on plan key
2401
- * @private
2402
- */
2403
- _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
2404
- const start = new Date(startDate);
2405
- const end = new Date(start);
2406
- switch (planKey) {
2407
- case "monthly":
2408
- end.setMonth(end.getMonth() + 1);
2409
- break;
2410
- case "quarterly":
2411
- end.setMonth(end.getMonth() + 3);
2412
- break;
2413
- case "yearly":
2414
- end.setFullYear(end.getFullYear() + 1);
2415
- break;
2416
- default:
2417
- end.setDate(end.getDate() + 30);
2418
- }
2419
- return end;
2420
- }
2421
- };
2422
- var PaymentService = class {
2423
- models;
2424
- providers;
2425
- config;
2426
- plugins;
2427
- logger;
2428
- events;
2429
- retryConfig;
2430
- circuitBreaker;
2431
- constructor(container) {
2432
- this.models = container.get("models");
2433
- this.providers = container.get("providers");
2434
- this.config = container.get("config");
2435
- this.plugins = container.get("plugins");
2436
- this.logger = container.get("logger");
2437
- this.events = container.get("events");
2438
- this.retryConfig = container.get("retryConfig");
2439
- this.circuitBreaker = container.get("circuitBreaker");
2440
- }
2441
- /**
2442
- * Create plugin context for hook execution
2443
- * @private
2444
- */
2445
- getPluginContext(idempotencyKey) {
2446
- return {
2447
- events: this.events,
2448
- logger: this.logger,
2449
- storage: /* @__PURE__ */ new Map(),
2450
- meta: {
2451
- idempotencyKey,
2452
- requestId: nanoid(),
2453
- timestamp: /* @__PURE__ */ new Date()
2454
- }
2455
- };
2456
- }
2457
- /**
2458
- * Execute provider call with retry and circuit breaker protection
2459
- * @private
2460
- */
2461
- async executeProviderCall(operation, operationName) {
2462
- const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
2463
- if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
2464
- return retry(withCircuitBreaker, {
2465
- ...this.retryConfig,
2466
- onRetry: (error, attempt, delay) => {
2467
- this.logger.warn(
2468
- `[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
2469
- error
2470
- );
2471
- this.retryConfig.onRetry?.(error, attempt, delay);
2472
- }
2473
- });
2474
- }
2475
- return withCircuitBreaker();
2476
- }
2477
- /**
2478
- * Verify a payment
2479
- *
2480
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
2481
- * @param options - Verification options
2482
- * @returns { transaction, status }
2483
- */
2484
- async verify(paymentIntentId, options = {}) {
2485
- return this.plugins.executeHook(
2486
- "payment.verify.before",
2487
- this.getPluginContext(),
2488
- { id: paymentIntentId, ...options },
2489
- async () => {
2490
- const { verifiedBy = null } = options;
2491
- const TransactionModel = this.models.Transaction;
2492
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
2493
- if (!transaction) {
2494
- throw new TransactionNotFoundError(paymentIntentId);
2495
- }
2496
- if (transaction.status === "verified" || transaction.status === "completed") {
2497
- throw new AlreadyVerifiedError(transaction._id.toString());
2498
- }
2499
- const gatewayType = transaction.gateway?.type ?? "manual";
2500
- const provider = this.providers[gatewayType];
2501
- if (!provider) {
2502
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
2503
- }
2504
- let paymentResult = null;
2505
- try {
2506
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
2507
- paymentResult = await this.executeProviderCall(
2508
- () => provider.verifyPayment(actualIntentId),
2509
- `${gatewayType}.verifyPayment`
2510
- );
2511
- } catch (error) {
2512
- this.logger.error("Payment verification failed:", error);
2513
- const auditEvent2 = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2514
- transaction.status,
2515
- "failed",
2516
- transaction._id.toString(),
2517
- {
2518
- changedBy: "system",
2519
- reason: `Payment verification failed: ${error.message}`,
2520
- metadata: { error: error.message }
2521
- }
2522
- );
2523
- transaction.status = "failed";
2524
- transaction.failureReason = error.message;
2525
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent2));
2526
- transaction.metadata = {
2527
- ...transaction.metadata,
2528
- verificationError: error.message,
2529
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
2530
- };
2531
- await transaction.save();
2532
- this.events.emit("payment.failed", {
2533
- transaction,
2534
- error: error.message,
2535
- provider: gatewayType,
2536
- paymentIntentId
2537
- });
2538
- throw new PaymentVerificationError(paymentIntentId, error.message);
2539
- }
2540
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
2541
- throw new ValidationError(
2542
- `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
2543
- { expected: transaction.amount, actual: paymentResult.amount }
2544
- );
2545
- }
2546
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
2547
- throw new ValidationError(
2548
- `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
2549
- { expected: transaction.currency, actual: paymentResult.currency }
2550
- );
2551
- }
2552
- const newStatus = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
2553
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2554
- transaction.status,
2555
- newStatus,
2556
- transaction._id.toString(),
2557
- {
2558
- changedBy: verifiedBy ?? "system",
2559
- reason: `Payment verification ${paymentResult.status === "succeeded" ? "succeeded" : "resulted in status: " + newStatus}`,
2560
- metadata: { paymentResult: paymentResult.metadata }
2561
- }
2562
- );
2563
- transaction.status = newStatus;
2564
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
2565
- transaction.verifiedBy = verifiedBy;
2566
- transaction.gateway = {
2567
- ...transaction.gateway,
2568
- type: transaction.gateway?.type ?? "manual",
2569
- verificationData: paymentResult.metadata
2570
- };
2571
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
2572
- await transaction.save();
2573
- if (newStatus === "verified") {
2574
- this.events.emit("payment.verified", {
2575
- transaction,
2576
- paymentResult,
2577
- verifiedBy: verifiedBy || void 0
2578
- });
2579
- } else if (newStatus === "failed") {
2580
- this.events.emit("payment.failed", {
2581
- transaction,
2582
- error: paymentResult.metadata?.errorMessage || "Payment verification failed",
2583
- provider: gatewayType,
2584
- paymentIntentId: transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId
2585
- });
2586
- } else if (newStatus === "requires_action") {
2587
- this.events.emit("payment.requires_action", {
2588
- transaction,
2589
- paymentResult,
2590
- action: paymentResult.metadata?.requiredAction
2591
- });
2592
- } else if (newStatus === "processing") {
2593
- this.events.emit("payment.processing", {
2594
- transaction,
2595
- paymentResult
2596
- });
2597
- }
2598
- const result = {
2599
- transaction,
2600
- paymentResult,
2601
- status: transaction.status
2602
- };
2603
- return this.plugins.executeHook(
2604
- "payment.verify.after",
2605
- this.getPluginContext(),
2606
- { id: paymentIntentId, ...options },
2607
- async () => result
2608
- );
2609
- }
2610
- );
2611
- }
2612
- /**
2613
- * Get payment status
2614
- *
2615
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
2616
- * @returns { transaction, status }
2617
- */
2618
- async getStatus(paymentIntentId) {
2619
- const TransactionModel = this.models.Transaction;
2620
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
2621
- if (!transaction) {
2622
- throw new TransactionNotFoundError(paymentIntentId);
2623
- }
2624
- const gatewayType = transaction.gateway?.type ?? "manual";
2625
- const provider = this.providers[gatewayType];
2626
- if (!provider) {
2627
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
2628
- }
2629
- let paymentResult = null;
2630
- try {
2631
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
2632
- paymentResult = await this.executeProviderCall(
2633
- () => provider.getStatus(actualIntentId),
2634
- `${gatewayType}.getStatus`
2635
- );
2636
- } catch (error) {
2637
- this.logger.warn("Failed to get payment status from provider:", error);
2638
- return {
2639
- transaction,
2640
- status: transaction.status,
2641
- provider: gatewayType
2642
- };
2643
- }
2644
- return {
2645
- transaction,
2646
- paymentResult,
2647
- status: paymentResult.status,
2648
- provider: gatewayType
2649
- };
2650
- }
2651
- /**
2652
- * Refund a payment
2653
- *
2654
- * @param paymentId - Payment intent ID, session ID, or transaction ID
2655
- * @param amount - Amount to refund (optional, full refund if not provided)
2656
- * @param options - Refund options
2657
- * @returns { transaction, refundResult }
2658
- */
2659
- async refund(paymentId, amount = null, options = {}) {
2660
- return this.plugins.executeHook(
2661
- "payment.refund.before",
2662
- this.getPluginContext(),
2663
- { transactionId: paymentId, amount, ...options },
2664
- async () => {
2665
- const { reason = null } = options;
2666
- const TransactionModel = this.models.Transaction;
2667
- const transaction = await this._findTransaction(TransactionModel, paymentId);
2668
- if (!transaction) {
2669
- throw new TransactionNotFoundError(paymentId);
2670
- }
2671
- if (transaction.status !== "verified" && transaction.status !== "completed" && transaction.status !== "partially_refunded") {
2672
- throw new InvalidStateTransitionError(
2673
- "transaction",
2674
- transaction._id.toString(),
2675
- transaction.status,
2676
- "verified, completed, or partially_refunded"
2677
- );
2678
- }
2679
- const gatewayType = transaction.gateway?.type ?? "manual";
2680
- const provider = this.providers[gatewayType];
2681
- if (!provider) {
2682
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
2683
- }
2684
- const capabilities = provider.getCapabilities();
2685
- if (!capabilities.supportsRefunds) {
2686
- throw new RefundNotSupportedError(gatewayType);
2687
- }
2688
- const refundedSoFar = transaction.refundedAmount ?? 0;
2689
- const refundableAmount = transaction.amount - refundedSoFar;
2690
- const refundAmount = amount ?? refundableAmount;
2691
- if (refundAmount <= 0) {
2692
- throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
2693
- }
2694
- if (refundAmount > refundableAmount) {
2695
- throw new ValidationError(
2696
- `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
2697
- { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
2698
- );
2699
- }
2700
- let refundResult;
2701
- try {
2702
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentId;
2703
- refundResult = await this.executeProviderCall(
2704
- () => provider.refund(actualIntentId, refundAmount, { reason: reason ?? void 0 }),
2705
- `${gatewayType}.refund`
2706
- );
2707
- } catch (error) {
2708
- this.logger.error("Refund failed:", error);
2709
- throw new RefundError(paymentId, error.message);
2710
- }
2711
- const refundFlow = this.config.transactionTypeMapping?.refund ?? TRANSACTION_FLOW.OUTFLOW;
2712
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
2713
- let refundTaxAmount = 0;
2714
- if (transaction.tax && transaction.tax > 0) {
2715
- if (transaction.amount > 0) {
2716
- const ratio = refundAmount / transaction.amount;
2717
- refundTaxAmount = Math.round(transaction.tax * ratio);
2718
- }
2719
- }
2720
- const refundFeeAmount = refundCommission?.gatewayFeeAmount || 0;
2721
- const refundNetAmount = refundAmount - refundFeeAmount - refundTaxAmount;
2722
- const refundTransaction = await TransactionModel.create({
2723
- organizationId: transaction.organizationId,
2724
- customerId: transaction.customerId,
2725
- // ✅ UNIFIED: Type = category (semantic), Flow = direction (from config)
2726
- type: "refund",
2727
- // Category: this is a refund transaction
2728
- flow: refundFlow,
2729
- // Direction: income or expense (from config)
2730
- tags: ["refund"],
2731
- // ✅ UNIFIED: Amount structure
2732
- amount: refundAmount,
2733
- currency: transaction.currency,
2734
- fee: refundFeeAmount,
2735
- tax: refundTaxAmount,
2736
- net: refundNetAmount,
2737
- // Tax details (if tax existed on original)
2738
- ...transaction.taxDetails && {
2739
- taxDetails: transaction.taxDetails
2740
- },
2741
- method: transaction.method ?? "manual",
2742
- status: "completed",
2743
- gateway: {
2744
- provider: transaction.gateway?.provider ?? "manual",
2745
- paymentIntentId: refundResult.id,
2746
- chargeId: refundResult.id
2747
- },
2748
- paymentDetails: transaction.paymentDetails,
2749
- // Reversed commission
2750
- ...refundCommission && { commission: refundCommission },
2751
- // ✅ UNIFIED: Source reference + related transaction
2752
- ...transaction.sourceId && { sourceId: transaction.sourceId },
2753
- ...transaction.sourceModel && { sourceModel: transaction.sourceModel },
2754
- relatedTransactionId: transaction._id,
2755
- // Link to original transaction
2756
- metadata: {
2757
- ...transaction.metadata,
2758
- isRefund: true,
2759
- originalTransactionId: transaction._id.toString(),
2760
- refundReason: reason,
2761
- refundResult: refundResult.metadata
2762
- },
2763
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
2764
- });
2765
- const isPartialRefund = refundAmount < refundableAmount;
2766
- const refundStatus = isPartialRefund ? "partially_refunded" : "refunded";
2767
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2768
- transaction.status,
2769
- refundStatus,
2770
- transaction._id.toString(),
2771
- {
2772
- changedBy: "system",
2773
- reason: `Refund processed: ${isPartialRefund ? "partial" : "full"} refund of ${refundAmount}${reason ? " - " + reason : ""}`,
2774
- metadata: {
2775
- refundAmount,
2776
- isPartialRefund,
2777
- refundTransactionId: refundTransaction._id.toString()
2778
- }
2779
- }
2780
- );
2781
- transaction.status = refundStatus;
2782
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
2783
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
2784
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
2785
- transaction.metadata = {
2786
- ...transaction.metadata,
2787
- refundTransactionId: refundTransaction._id.toString(),
2788
- refundReason: reason
2789
- };
2790
- await transaction.save();
2791
- this.events.emit("payment.refunded", {
2792
- transaction,
2793
- refundTransaction,
2794
- refundResult: {
2795
- ...refundResult,
2796
- currency: refundResult.currency ?? "USD",
2797
- metadata: refundResult.metadata ?? {}
2798
- },
2799
- refundAmount,
2800
- reason: reason ?? void 0,
2801
- isPartialRefund
2802
- });
2803
- const result = {
2804
- transaction,
2805
- refundTransaction,
2806
- refundResult,
2807
- status: transaction.status
2808
- };
2809
- return this.plugins.executeHook(
2810
- "payment.refund.after",
2811
- this.getPluginContext(),
2812
- { transactionId: paymentId, amount, ...options },
2813
- async () => result
2814
- );
2815
- }
2816
- );
2817
- }
2818
- /**
2819
- * Handle webhook from payment provider
2820
- *
2821
- * @param provider - Provider name
2822
- * @param payload - Webhook payload
2823
- * @param headers - Request headers
2824
- * @returns { event, transaction }
2825
- */
2826
- async handleWebhook(providerName, payload, headers = {}) {
2827
- const provider = this.providers[providerName];
2828
- if (!provider) {
2829
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
2830
- }
2831
- const capabilities = provider.getCapabilities();
2832
- if (!capabilities.supportsWebhooks) {
2833
- throw new ProviderCapabilityError(providerName, "webhooks");
2834
- }
2835
- let webhookEvent;
2836
- try {
2837
- webhookEvent = await this.executeProviderCall(
2838
- () => provider.handleWebhook(payload, headers),
2839
- `${providerName}.handleWebhook`
2840
- );
2841
- } catch (error) {
2842
- this.logger.error("Webhook processing failed:", error);
2843
- throw new ProviderError(
2844
- `Webhook processing failed for ${providerName}: ${error.message}`,
2845
- "WEBHOOK_PROCESSING_FAILED",
2846
- { retryable: false }
2847
- );
2848
- }
2849
- if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) {
2850
- throw new ValidationError(
2851
- `Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`,
2852
- { provider: providerName, eventType: webhookEvent?.type }
2853
- );
2854
- }
2855
- const TransactionModel = this.models.Transaction;
2856
- let transaction = null;
2857
- if (webhookEvent.data.sessionId) {
2858
- transaction = await TransactionModel.findOne({
2859
- "gateway.sessionId": webhookEvent.data.sessionId
2860
- });
2861
- }
2862
- if (!transaction && webhookEvent.data.paymentIntentId) {
2863
- transaction = await TransactionModel.findOne({
2864
- "gateway.paymentIntentId": webhookEvent.data.paymentIntentId
2865
- });
2866
- }
2867
- if (!transaction) {
2868
- this.logger.warn("Transaction not found for webhook event", {
2869
- provider: providerName,
2870
- eventId: webhookEvent.id,
2871
- sessionId: webhookEvent.data.sessionId,
2872
- paymentIntentId: webhookEvent.data.paymentIntentId
2873
- });
2874
- throw new TransactionNotFoundError(
2875
- webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown"
2876
- );
2877
- }
2878
- if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) {
2879
- transaction.gateway = {
2880
- ...transaction.gateway,
2881
- type: transaction.gateway?.type ?? "manual",
2882
- sessionId: webhookEvent.data.sessionId
2883
- };
2884
- }
2885
- if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) {
2886
- transaction.gateway = {
2887
- ...transaction.gateway,
2888
- type: transaction.gateway?.type ?? "manual",
2889
- paymentIntentId: webhookEvent.data.paymentIntentId
2890
- };
2891
- }
2892
- if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
2893
- this.logger.warn("Webhook already processed", {
2894
- transactionId: transaction._id,
2895
- eventId: webhookEvent.id
2896
- });
2897
- return {
2898
- event: webhookEvent,
2899
- transaction,
2900
- status: "already_processed"
2901
- };
2902
- }
2903
- transaction.webhook = {
2904
- eventId: webhookEvent.id,
2905
- eventType: webhookEvent.type,
2906
- receivedAt: /* @__PURE__ */ new Date(),
2907
- processedAt: /* @__PURE__ */ new Date(),
2908
- data: webhookEvent.data
2909
- };
2910
- let newStatus = transaction.status;
2911
- if (webhookEvent.type === "payment.succeeded") {
2912
- newStatus = "verified";
2913
- } else if (webhookEvent.type === "payment.failed") {
2914
- newStatus = "failed";
2915
- } else if (webhookEvent.type === "refund.succeeded") {
2916
- newStatus = "refunded";
2917
- }
2918
- if (newStatus !== transaction.status) {
2919
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2920
- transaction.status,
2921
- newStatus,
2922
- transaction._id.toString(),
2923
- {
2924
- changedBy: "webhook",
2925
- reason: `Webhook event: ${webhookEvent.type}`,
2926
- metadata: {
2927
- webhookId: webhookEvent.id,
2928
- webhookType: webhookEvent.type,
2929
- webhookData: webhookEvent.data
2930
- }
2931
- }
2932
- );
2933
- transaction.status = newStatus;
2934
- if (newStatus === "verified") {
2935
- transaction.verifiedAt = webhookEvent.createdAt;
2936
- } else if (newStatus === "refunded") {
2937
- transaction.refundedAt = webhookEvent.createdAt;
2938
- } else if (newStatus === "failed") {
2939
- transaction.failedAt = webhookEvent.createdAt;
2940
- }
2941
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
2942
- }
2943
- await transaction.save();
2944
- this.events.emit("webhook.processed", {
2945
- webhookType: webhookEvent.type,
2946
- provider: webhookEvent.provider,
2947
- event: webhookEvent,
2948
- transaction,
2949
- processedAt: /* @__PURE__ */ new Date()
2950
- });
2951
- return {
2952
- event: webhookEvent,
2953
- transaction,
2954
- status: "processed"
2955
- };
2956
- }
2957
- /**
2958
- * List payments/transactions with filters
2959
- *
2960
- * @param filters - Query filters
2961
- * @param options - Query options (limit, skip, sort)
2962
- * @returns Transactions
2963
- */
2964
- async list(filters = {}, options = {}) {
2965
- const TransactionModel = this.models.Transaction;
2966
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
2967
- const transactions = await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
2968
- return transactions;
2969
- }
2970
- /**
2971
- * Get payment/transaction by ID
2972
- *
2973
- * @param transactionId - Transaction ID
2974
- * @returns Transaction
2975
- */
2976
- async get(transactionId) {
2977
- const TransactionModel = this.models.Transaction;
2978
- const transaction = await TransactionModel.findById(transactionId);
2979
- if (!transaction) {
2980
- throw new TransactionNotFoundError(transactionId);
2981
- }
2982
- return transaction;
2983
- }
2984
- /**
2985
- * Get provider instance
2986
- *
2987
- * @param providerName - Provider name
2988
- * @returns Provider instance
2989
- */
2990
- getProvider(providerName) {
2991
- const provider = this.providers[providerName];
2992
- if (!provider) {
2993
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
2994
- }
2995
- return provider;
2996
- }
2997
- /**
2998
- * Find transaction by sessionId, paymentIntentId, or transaction ID
2999
- * @private
3000
- */
3001
- async _findTransaction(TransactionModel, identifier) {
3002
- let transaction = await TransactionModel.findOne({
3003
- "gateway.sessionId": identifier
3004
- });
3005
- if (!transaction) {
3006
- transaction = await TransactionModel.findOne({
3007
- "gateway.paymentIntentId": identifier
3008
- });
3009
- }
3010
- if (!transaction) {
3011
- transaction = await TransactionModel.findById(identifier);
3012
- }
3013
- return transaction;
3014
- }
3015
- };
3016
- var TransactionService = class {
3017
- models;
3018
- plugins;
3019
- events;
3020
- logger;
3021
- constructor(container) {
3022
- this.models = container.get("models");
3023
- this.plugins = container.get("plugins");
3024
- this.events = container.get("events");
3025
- this.logger = container.get("logger");
3026
- }
3027
- /**
3028
- * Create plugin context for hook execution
3029
- * @private
3030
- */
3031
- getPluginContext() {
3032
- return {
3033
- events: this.events,
3034
- logger: this.logger,
3035
- storage: /* @__PURE__ */ new Map(),
3036
- meta: {
3037
- requestId: nanoid(),
3038
- timestamp: /* @__PURE__ */ new Date()
3039
- }
3040
- };
3041
- }
3042
- /**
3043
- * Get transaction by ID
3044
- *
3045
- * @param transactionId - Transaction ID
3046
- * @returns Transaction
3047
- */
3048
- async get(transactionId) {
3049
- const TransactionModel = this.models.Transaction;
3050
- const transaction = await TransactionModel.findById(transactionId);
3051
- if (!transaction) {
3052
- throw new TransactionNotFoundError(transactionId);
3053
- }
3054
- return transaction;
3055
- }
3056
- /**
3057
- * List transactions with filters
3058
- *
3059
- * @param filters - Query filters
3060
- * @param options - Query options (limit, skip, sort, populate)
3061
- * @returns { transactions, total, page, limit }
3062
- */
3063
- async list(filters = {}, options = {}) {
3064
- const TransactionModel = this.models.Transaction;
3065
- const {
3066
- limit = 50,
3067
- skip = 0,
3068
- page = null,
3069
- sort = { createdAt: -1 },
3070
- populate = []
3071
- } = options;
3072
- const actualSkip = page ? (page - 1) * limit : skip;
3073
- let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
3074
- if (populate.length > 0 && typeof query.populate === "function") {
3075
- populate.forEach((field) => {
3076
- query = query.populate(field);
3077
- });
3078
- }
3079
- const transactions = await query;
3080
- const model = TransactionModel;
3081
- const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
3082
- return {
3083
- transactions,
3084
- total,
3085
- page: page ?? Math.floor(actualSkip / limit) + 1,
3086
- limit,
3087
- pages: Math.ceil(total / limit)
3088
- };
3089
- }
3090
- /**
3091
- * Update transaction
3092
- *
3093
- * @param transactionId - Transaction ID
3094
- * @param updates - Fields to update
3095
- * @returns Updated transaction
3096
- */
3097
- async update(transactionId, updates) {
3098
- const hookInput = { transactionId, updates };
3099
- return this.plugins.executeHook(
3100
- "transaction.update.before",
3101
- this.getPluginContext(),
3102
- hookInput,
3103
- async () => {
3104
- const TransactionModel = this.models.Transaction;
3105
- const effectiveUpdates = hookInput.updates;
3106
- const model = TransactionModel;
3107
- let transaction;
3108
- if (typeof model.update === "function") {
3109
- transaction = await model.update(transactionId, effectiveUpdates);
3110
- } else if (typeof model.findByIdAndUpdate === "function") {
3111
- transaction = await model.findByIdAndUpdate(
3112
- transactionId,
3113
- { $set: effectiveUpdates },
3114
- { new: true }
3115
- );
3116
- } else {
3117
- throw new Error("Transaction model does not support update operations");
3118
- }
3119
- if (!transaction) {
3120
- throw new TransactionNotFoundError(transactionId);
3121
- }
3122
- this.events.emit("transaction.updated", {
3123
- transaction,
3124
- updates: effectiveUpdates
3125
- });
3126
- return this.plugins.executeHook(
3127
- "transaction.update.after",
3128
- this.getPluginContext(),
3129
- { transactionId, updates: effectiveUpdates },
3130
- async () => transaction
3131
- );
3132
- }
3133
- );
3134
- }
3135
- };
3136
-
3137
- // src/shared/utils/calculators/commission-split.ts
3138
- function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
3139
- if (!splitRules || splitRules.length === 0) {
3140
- return [];
3141
- }
3142
- if (amount < 0) {
3143
- throw new Error("Transaction amount cannot be negative");
3144
- }
3145
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
3146
- throw new Error("Gateway fee rate must be between 0 and 1");
3147
- }
3148
- const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
3149
- if (totalRate > 1) {
3150
- throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
3151
- }
3152
- return splitRules.map((rule, index) => {
3153
- if (rule.rate < 0 || rule.rate > 1) {
3154
- throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
3155
- }
3156
- const grossAmount = Math.round(amount * rule.rate);
3157
- const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate) : 0;
3158
- const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
3159
- return {
3160
- type: rule.type ?? SPLIT_TYPE.CUSTOM,
3161
- recipientId: rule.recipientId,
3162
- recipientType: rule.recipientType,
3163
- rate: rule.rate,
3164
- grossAmount,
3165
- gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
3166
- gatewayFeeAmount,
3167
- netAmount,
3168
- status: SPLIT_STATUS.PENDING,
3169
- dueDate: rule.dueDate ?? null,
3170
- metadata: rule.metadata ?? {}
3171
- };
3172
- });
3173
- }
3174
- function calculateOrganizationPayout(amount, splits = []) {
3175
- const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
3176
- return Math.max(0, amount - totalSplitAmount);
3177
- }
3178
-
3179
- // src/application/services/escrow.service.ts
3180
- var EscrowService = class {
3181
- models;
3182
- plugins;
3183
- logger;
3184
- events;
3185
- constructor(container) {
3186
- this.models = container.get("models");
3187
- this.plugins = container.get("plugins");
3188
- this.logger = container.get("logger");
3189
- this.events = container.get("events");
3190
- }
3191
- /**
3192
- * Create plugin context for hook execution
3193
- * @private
3194
- */
3195
- getPluginContext() {
3196
- return {
3197
- events: this.events,
3198
- logger: this.logger,
3199
- storage: /* @__PURE__ */ new Map(),
3200
- meta: {
3201
- requestId: nanoid(),
3202
- timestamp: /* @__PURE__ */ new Date()
3203
- }
3204
- };
3205
- }
3206
- /**
3207
- * Hold funds in escrow
3208
- *
3209
- * @param transactionId - Transaction to hold
3210
- * @param options - Hold options
3211
- * @returns Updated transaction
3212
- */
3213
- async hold(transactionId, options = {}) {
3214
- return this.plugins.executeHook(
3215
- "escrow.hold.before",
3216
- this.getPluginContext(),
3217
- { transactionId, ...options },
3218
- async () => {
3219
- const {
3220
- reason = HOLD_REASON.PAYMENT_VERIFICATION,
3221
- holdUntil = null,
3222
- metadata = {}
3223
- } = options;
3224
- const TransactionModel = this.models.Transaction;
3225
- const transaction = await TransactionModel.findById(transactionId);
3226
- if (!transaction) {
3227
- throw new TransactionNotFoundError(transactionId);
3228
- }
3229
- if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
3230
- throw new InvalidStateTransitionError(
3231
- "transaction",
3232
- transaction._id.toString(),
3233
- transaction.status,
3234
- TRANSACTION_STATUS.VERIFIED
3235
- );
3236
- }
3237
- const heldAmount = transaction.amount;
3238
- transaction.hold = {
3239
- status: HOLD_STATUS.HELD,
3240
- heldAmount,
3241
- releasedAmount: 0,
3242
- reason,
3243
- heldAt: /* @__PURE__ */ new Date(),
3244
- ...holdUntil && { holdUntil },
3245
- releases: [],
3246
- metadata
3247
- };
3248
- await transaction.save();
3249
- this.events.emit("escrow.held", {
3250
- transaction,
3251
- heldAmount,
3252
- reason
3253
- });
3254
- return this.plugins.executeHook(
3255
- "escrow.hold.after",
3256
- this.getPluginContext(),
3257
- { transactionId, ...options },
3258
- async () => transaction
3259
- );
3260
- }
3261
- );
3262
- }
3263
- /**
3264
- * Release funds from escrow to recipient
3265
- *
3266
- * @param transactionId - Transaction to release
3267
- * @param options - Release options
3268
- * @returns { transaction, releaseTransaction }
3269
- */
3270
- async release(transactionId, options) {
3271
- return this.plugins.executeHook(
3272
- "escrow.release.before",
3273
- this.getPluginContext(),
3274
- { transactionId, ...options },
3275
- async () => {
3276
- const {
3277
- amount = null,
3278
- recipientId,
3279
- recipientType = "organization",
3280
- reason = RELEASE_REASON.PAYMENT_VERIFIED,
3281
- releasedBy = null,
3282
- createTransaction = true,
3283
- metadata = {}
3284
- } = options;
3285
- const TransactionModel = this.models.Transaction;
3286
- const transaction = await TransactionModel.findById(transactionId);
3287
- if (!transaction) {
3288
- throw new TransactionNotFoundError(transactionId);
3289
- }
3290
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
3291
- throw new InvalidStateTransitionError(
3292
- "escrow_hold",
3293
- transaction._id.toString(),
3294
- transaction.hold?.status ?? "none",
3295
- HOLD_STATUS.HELD
3296
- );
3297
- }
3298
- if (!recipientId) {
3299
- throw new ValidationError("recipientId is required for release", { transactionId });
3300
- }
3301
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
3302
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
3303
- if (releaseAmount > availableAmount) {
3304
- throw new ValidationError(
3305
- `Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`,
3306
- { releaseAmount, availableAmount, transactionId }
3307
- );
3308
- }
3309
- const releaseRecord = {
3310
- amount: releaseAmount,
3311
- recipientId,
3312
- recipientType,
3313
- releasedAt: /* @__PURE__ */ new Date(),
3314
- releasedBy,
3315
- reason,
3316
- metadata
3317
- };
3318
- transaction.hold.releases.push(releaseRecord);
3319
- transaction.hold.releasedAmount += releaseAmount;
3320
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
3321
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
3322
- if (isFullRelease) {
3323
- const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
3324
- transaction.hold.status,
3325
- HOLD_STATUS.RELEASED,
3326
- transaction._id.toString(),
3327
- {
3328
- changedBy: releasedBy ?? "system",
3329
- reason: `Escrow hold fully released: ${releaseAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
3330
- metadata: { releaseAmount, recipientId, releaseReason: reason }
3331
- }
3332
- );
3333
- transaction.hold.status = HOLD_STATUS.RELEASED;
3334
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
3335
- const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
3336
- transaction.status,
3337
- TRANSACTION_STATUS.COMPLETED,
3338
- transaction._id.toString(),
3339
- {
3340
- changedBy: releasedBy ?? "system",
3341
- reason: `Transaction completed after full escrow release`,
3342
- metadata: { releaseAmount, recipientId }
3343
- }
3344
- );
3345
- transaction.status = TRANSACTION_STATUS.COMPLETED;
3346
- Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
3347
- Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
3348
- } else if (isPartialRelease) {
3349
- const auditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
3350
- transaction.hold.status,
3351
- HOLD_STATUS.PARTIALLY_RELEASED,
3352
- transaction._id.toString(),
3353
- {
3354
- changedBy: releasedBy ?? "system",
3355
- reason: `Partial escrow release: ${releaseAmount} of ${transaction.hold.heldAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
3356
- metadata: {
3357
- releaseAmount,
3358
- recipientId,
3359
- releaseReason: reason,
3360
- remainingHeld: transaction.hold.heldAmount - transaction.hold.releasedAmount
3361
- }
3362
- }
3363
- );
3364
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
3365
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
3366
- }
3367
- if ("markModified" in transaction) {
3368
- transaction.markModified("hold");
3369
- }
3370
- await transaction.save();
3371
- let releaseTaxAmount = 0;
3372
- if (transaction.tax && transaction.tax > 0) {
3373
- if (releaseAmount === availableAmount && !amount) {
3374
- const releasedTaxSoFar = transaction.hold.releasedTaxAmount ?? 0;
3375
- releaseTaxAmount = transaction.tax - releasedTaxSoFar;
3376
- } else {
3377
- const totalAmount = transaction.amount + transaction.tax;
3378
- if (totalAmount > 0) {
3379
- const taxRatio = transaction.tax / totalAmount;
3380
- releaseTaxAmount = Math.round(releaseAmount * taxRatio);
3381
- }
3382
- }
3383
- }
3384
- const releaseNetAmount = releaseAmount - releaseTaxAmount;
3385
- let releaseTransaction = null;
3386
- if (createTransaction) {
3387
- releaseTransaction = await TransactionModel.create({
3388
- organizationId: transaction.organizationId,
3389
- customerId: recipientId,
3390
- // ✅ UNIFIED: Use 'escrow_release' as type, inflow
3391
- type: "escrow_release",
3392
- flow: "inflow",
3393
- tags: ["escrow", "release"],
3394
- // ✅ UNIFIED: Amount structure
3395
- amount: releaseAmount,
3396
- currency: transaction.currency,
3397
- fee: 0,
3398
- // No processing fees on releases
3399
- tax: releaseTaxAmount,
3400
- // ✅ Top-level number
3401
- net: releaseNetAmount,
3402
- // Copy tax details from original transaction
3403
- ...transaction.taxDetails && {
3404
- taxDetails: transaction.taxDetails
3405
- },
3406
- method: transaction.method,
3407
- status: "completed",
3408
- gateway: transaction.gateway,
3409
- // ✅ UNIFIED: Source reference (link to held transaction)
3410
- sourceId: transaction._id,
3411
- sourceModel: "Transaction",
3412
- relatedTransactionId: transaction._id,
3413
- metadata: {
3414
- ...metadata,
3415
- isRelease: true,
3416
- heldTransactionId: transaction._id.toString(),
3417
- releaseReason: reason,
3418
- recipientType,
3419
- // Store original category for reference
3420
- originalCategory: transaction.category
3421
- },
3422
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
3423
- });
3424
- }
3425
- this.events.emit("escrow.released", {
3426
- transaction,
3427
- releaseTransaction,
3428
- releaseAmount,
3429
- recipientId,
3430
- recipientType,
3431
- reason,
3432
- isFullRelease,
3433
- isPartialRelease
3434
- });
3435
- const result = {
3436
- transaction,
3437
- releaseTransaction,
3438
- releaseAmount,
3439
- isFullRelease,
3440
- isPartialRelease
3441
- };
3442
- return this.plugins.executeHook(
3443
- "escrow.release.after",
3444
- this.getPluginContext(),
3445
- { transactionId, ...options },
3446
- async () => result
3447
- );
3448
- }
3449
- );
3450
- }
3451
- /**
3452
- * Cancel hold and release back to customer
3453
- *
3454
- * @param transactionId - Transaction to cancel hold
3455
- * @param options - Cancel options
3456
- * @returns Updated transaction
3457
- */
3458
- async cancel(transactionId, options = {}) {
3459
- const { reason = "Hold cancelled", metadata = {} } = options;
3460
- const TransactionModel = this.models.Transaction;
3461
- const transaction = await TransactionModel.findById(transactionId);
3462
- if (!transaction) {
3463
- throw new TransactionNotFoundError(transactionId);
3464
- }
3465
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
3466
- throw new InvalidStateTransitionError(
3467
- "escrow_hold",
3468
- transaction._id.toString(),
3469
- transaction.hold?.status ?? "none",
3470
- HOLD_STATUS.HELD
3471
- );
3472
- }
3473
- const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
3474
- transaction.hold.status,
3475
- HOLD_STATUS.CANCELLED,
3476
- transaction._id.toString(),
3477
- {
3478
- changedBy: "system",
3479
- reason: `Escrow hold cancelled${reason ? ": " + reason : ""}`,
3480
- metadata: { cancelReason: reason, ...metadata }
3481
- }
3482
- );
3483
- transaction.hold.status = HOLD_STATUS.CANCELLED;
3484
- transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
3485
- transaction.hold.metadata = {
3486
- ...transaction.hold.metadata,
3487
- ...metadata,
3488
- cancelReason: reason
3489
- };
3490
- const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
3491
- transaction.status,
3492
- TRANSACTION_STATUS.CANCELLED,
3493
- transaction._id.toString(),
3494
- {
3495
- changedBy: "system",
3496
- reason: `Transaction cancelled due to escrow hold cancellation`,
3497
- metadata: { cancelReason: reason }
3498
- }
3499
- );
3500
- transaction.status = TRANSACTION_STATUS.CANCELLED;
3501
- Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
3502
- Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
3503
- if ("markModified" in transaction) {
3504
- transaction.markModified("hold");
3505
- }
3506
- await transaction.save();
3507
- this.events.emit("escrow.cancelled", {
3508
- transaction,
3509
- reason
3510
- });
3511
- return transaction;
3512
- }
3513
- /**
3514
- * Split payment to multiple recipients
3515
- * Deducts splits from held amount and releases remainder to organization
3516
- *
3517
- * @param transactionId - Transaction to split
3518
- * @param splitRules - Split configuration
3519
- * @returns { transaction, splitTransactions, organizationTransaction }
3520
- */
3521
- async split(transactionId, splitRules = []) {
3522
- const TransactionModel = this.models.Transaction;
3523
- const transaction = await TransactionModel.findById(transactionId);
3524
- if (!transaction) {
3525
- throw new TransactionNotFoundError(transactionId);
3526
- }
3527
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
3528
- throw new InvalidStateTransitionError(
3529
- "escrow_hold",
3530
- transaction._id.toString(),
3531
- transaction.hold?.status ?? "none",
3532
- HOLD_STATUS.HELD
3533
- );
3534
- }
3535
- if (!splitRules || splitRules.length === 0) {
3536
- throw new ValidationError("splitRules cannot be empty", { transactionId });
3537
- }
3538
- const splits = calculateSplits(
3539
- transaction.amount,
3540
- splitRules,
3541
- transaction.commission?.gatewayFeeRate ?? 0
3542
- );
3543
- transaction.splits = splits;
3544
- await transaction.save();
3545
- const splitTransactions = [];
3546
- const totalTax = transaction.tax ?? 0;
3547
- const totalBaseAmount = transaction.amount;
3548
- let allocatedTaxAmount = 0;
3549
- const splitTaxAmounts = splits.map((split) => {
3550
- if (!totalTax || totalBaseAmount <= 0) {
3551
- return 0;
3552
- }
3553
- const ratio = split.grossAmount / totalBaseAmount;
3554
- const taxAmount = Math.round(totalTax * ratio);
3555
- allocatedTaxAmount += taxAmount;
3556
- return taxAmount;
3557
- });
3558
- for (const [index, split] of splits.entries()) {
3559
- const splitTaxAmount = totalTax > 0 ? splitTaxAmounts[index] ?? 0 : 0;
3560
- const splitNetAmount = split.grossAmount - split.gatewayFeeAmount - splitTaxAmount;
3561
- const splitTransaction = await TransactionModel.create({
3562
- organizationId: transaction.organizationId,
3563
- customerId: split.recipientId,
3564
- // ✅ UNIFIED: Use split type directly (commission, platform_fee, etc.)
3565
- type: split.type,
3566
- flow: "outflow",
3567
- // Splits are money going out
3568
- tags: ["split", "commission"],
3569
- // ✅ UNIFIED: Amount structure (gross, fee, tax, net)
3570
- amount: split.grossAmount,
3571
- // ✅ Gross amount (before deductions)
3572
- currency: transaction.currency,
3573
- fee: split.gatewayFeeAmount,
3574
- tax: splitTaxAmount,
3575
- // ✅ Top-level number
3576
- net: splitNetAmount,
3577
- // ✅ Net = gross - fee - tax
3578
- // Copy tax details from original transaction (if applicable)
3579
- ...transaction.taxDetails && splitTaxAmount > 0 && {
3580
- taxDetails: transaction.taxDetails
3581
- },
3582
- method: transaction.method,
3583
- status: "completed",
3584
- gateway: transaction.gateway,
3585
- // ✅ UNIFIED: Source reference (link to original transaction)
3586
- sourceId: transaction._id,
3587
- sourceModel: "Transaction",
3588
- relatedTransactionId: transaction._id,
3589
- metadata: {
3590
- isSplit: true,
3591
- splitType: split.type,
3592
- recipientType: split.recipientType,
3593
- originalTransactionId: transaction._id.toString(),
3594
- // Store split details for reference
3595
- splitGrossAmount: split.grossAmount,
3596
- splitNetAmount: split.netAmount,
3597
- // Original calculation
3598
- gatewayFeeAmount: split.gatewayFeeAmount
3599
- },
3600
- idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
3601
- });
3602
- split.payoutTransactionId = splitTransaction._id.toString();
3603
- split.status = SPLIT_STATUS.PAID;
3604
- split.paidDate = /* @__PURE__ */ new Date();
3605
- splitTransactions.push(splitTransaction);
3606
- }
3607
- await transaction.save();
3608
- const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
3609
- const organizationTaxAmount = totalTax > 0 ? Math.max(0, totalTax - allocatedTaxAmount) : 0;
3610
- const organizationPayoutTotal = totalTax > 0 ? organizationPayout + organizationTaxAmount : organizationPayout;
3611
- const organizationTransaction = await this.release(transactionId, {
3612
- amount: organizationPayoutTotal,
3613
- recipientId: transaction.organizationId?.toString() ?? "",
3614
- recipientType: "organization",
3615
- reason: RELEASE_REASON.PAYMENT_VERIFIED,
3616
- createTransaction: true,
3617
- metadata: {
3618
- afterSplits: true,
3619
- totalSplits: splits.length,
3620
- totalSplitAmount: transaction.amount - organizationPayout
3621
- }
3622
- });
3623
- this.events.emit("escrow.split", {
3624
- transaction,
3625
- splits,
3626
- splitTransactions,
3627
- organizationTransaction: organizationTransaction.releaseTransaction,
3628
- organizationPayout
3629
- });
3630
- return {
3631
- transaction,
3632
- splits,
3633
- splitTransactions,
3634
- organizationTransaction: organizationTransaction.releaseTransaction,
3635
- organizationPayout
3636
- };
3637
- }
3638
- /**
3639
- * Get escrow status
3640
- *
3641
- * @param transactionId - Transaction ID
3642
- * @returns Escrow status
3643
- */
3644
- async getStatus(transactionId) {
3645
- const TransactionModel = this.models.Transaction;
3646
- const transaction = await TransactionModel.findById(transactionId);
3647
- if (!transaction) {
3648
- throw new TransactionNotFoundError(transactionId);
3649
- }
3650
- return {
3651
- transaction,
3652
- hold: transaction.hold ?? null,
3653
- splits: transaction.splits ?? [],
3654
- hasHold: !!transaction.hold,
3655
- hasSplits: transaction.splits ? transaction.splits.length > 0 : false
3656
- };
3657
- }
3658
- };
3659
-
3660
- // src/application/services/settlement.service.ts
3661
- var SettlementService = class {
3662
- models;
3663
- plugins;
3664
- logger;
3665
- events;
3666
- constructor(container) {
3667
- this.models = container.get("models");
3668
- this.plugins = container.get("plugins");
3669
- this.logger = container.get("logger");
3670
- this.events = container.get("events");
3671
- void this.plugins;
3672
- }
3673
- /**
3674
- * Create settlements from transaction splits
3675
- * Typically called after escrow is released
3676
- *
3677
- * @param transactionId - Transaction ID with splits
3678
- * @param options - Creation options
3679
- * @returns Array of created settlements
3680
- */
3681
- async createFromSplits(transactionId, options = {}) {
3682
- const {
3683
- scheduledAt = /* @__PURE__ */ new Date(),
3684
- payoutMethod = "bank_transfer",
3685
- metadata = {}
3686
- } = options;
3687
- if (!this.models.Settlement) {
3688
- throw new ModelNotRegisteredError("Settlement");
3689
- }
3690
- const TransactionModel = this.models.Transaction;
3691
- const transaction = await TransactionModel.findById(transactionId);
3692
- if (!transaction) {
3693
- throw new TransactionNotFoundError(transactionId);
3694
- }
3695
- if (!transaction.splits || transaction.splits.length === 0) {
3696
- throw new ValidationError("Transaction has no splits to settle", { transactionId });
3697
- }
3698
- const SettlementModel = this.models.Settlement;
3699
- const settlements = [];
3700
- for (const split of transaction.splits) {
3701
- if (split.status === "paid") {
3702
- this.logger.info("Split already paid, skipping", { splitId: split._id });
3703
- continue;
3704
- }
3705
- const settlement = await SettlementModel.create({
3706
- organizationId: transaction.organizationId,
3707
- recipientId: split.recipientId,
3708
- recipientType: split.recipientType,
3709
- type: SETTLEMENT_TYPE.SPLIT_PAYOUT,
3710
- status: SETTLEMENT_STATUS.PENDING,
3711
- payoutMethod,
3712
- amount: split.netAmount,
3713
- currency: transaction.currency,
3714
- sourceTransactionIds: [transaction._id],
3715
- sourceSplitIds: [split._id?.toString() || ""],
3716
- scheduledAt,
3717
- metadata: {
3718
- ...metadata,
3719
- splitType: split.type,
3720
- transactionCategory: transaction.category
3721
- }
3722
- });
3723
- settlements.push(settlement);
3724
- }
3725
- this.events.emit("settlement.created", {
3726
- settlements,
3727
- transactionId,
3728
- count: settlements.length
3729
- });
3730
- this.logger.info("Created settlements from splits", {
3731
- transactionId,
3732
- count: settlements.length
3733
- });
3734
- return settlements;
3735
- }
3736
- /**
3737
- * Schedule a payout
3738
- *
3739
- * @param params - Settlement parameters
3740
- * @returns Created settlement
3741
- */
3742
- async schedule(params) {
3743
- if (!this.models.Settlement) {
3744
- throw new ModelNotRegisteredError("Settlement");
3745
- }
3746
- const {
3747
- organizationId,
3748
- recipientId,
3749
- recipientType,
3750
- type,
3751
- amount,
3752
- currency = "USD",
3753
- payoutMethod,
3754
- sourceTransactionIds = [],
3755
- sourceSplitIds = [],
3756
- scheduledAt = /* @__PURE__ */ new Date(),
3757
- bankTransferDetails,
3758
- mobileWalletDetails,
3759
- cryptoDetails,
3760
- notes,
3761
- metadata = {}
3762
- } = params;
3763
- if (amount <= 0) {
3764
- throw new ValidationError("Settlement amount must be positive", { amount });
3765
- }
3766
- const SettlementModel = this.models.Settlement;
3767
- const settlement = await SettlementModel.create({
3768
- organizationId,
3769
- recipientId,
3770
- recipientType,
3771
- type,
3772
- status: SETTLEMENT_STATUS.PENDING,
3773
- payoutMethod,
3774
- amount,
3775
- currency,
3776
- sourceTransactionIds,
3777
- sourceSplitIds,
3778
- scheduledAt,
3779
- bankTransferDetails,
3780
- mobileWalletDetails,
3781
- cryptoDetails,
3782
- notes,
3783
- metadata
3784
- });
3785
- this.events.emit("settlement.scheduled", {
3786
- settlement,
3787
- scheduledAt
3788
- });
3789
- this.logger.info("Settlement scheduled", {
3790
- settlementId: settlement._id,
3791
- recipientId,
3792
- amount
3793
- });
3794
- return settlement;
3795
- }
3796
- /**
3797
- * Process pending settlements
3798
- * Batch process settlements that are due
3799
- *
3800
- * @param options - Processing options
3801
- * @returns Processing result
3802
- */
3803
- async processPending(options = {}) {
3804
- if (!this.models.Settlement) {
3805
- throw new ModelNotRegisteredError("Settlement");
3806
- }
3807
- const {
3808
- limit = 100,
3809
- organizationId,
3810
- payoutMethod,
3811
- dryRun = false
3812
- } = options;
3813
- const SettlementModel = this.models.Settlement;
3814
- const query = {
3815
- status: SETTLEMENT_STATUS.PENDING,
3816
- scheduledAt: { $lte: /* @__PURE__ */ new Date() }
3817
- };
3818
- if (organizationId) query.organizationId = organizationId;
3819
- if (payoutMethod) query.payoutMethod = payoutMethod;
3820
- const settlements = await SettlementModel.find(query).limit(limit).sort({ scheduledAt: 1 });
3821
- const result = {
3822
- processed: 0,
3823
- succeeded: 0,
3824
- failed: 0,
3825
- settlements: [],
3826
- errors: []
3827
- };
3828
- if (dryRun) {
3829
- this.logger.info("Dry run: would process settlements", { count: settlements.length });
3830
- result.settlements = settlements;
3831
- return result;
3832
- }
3833
- for (const settlement of settlements) {
3834
- result.processed++;
3835
- try {
3836
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3837
- settlement.status,
3838
- SETTLEMENT_STATUS.PROCESSING,
3839
- settlement._id.toString(),
3840
- {
3841
- changedBy: "system",
3842
- reason: "Settlement processing started",
3843
- metadata: { recipientId: settlement.recipientId, amount: settlement.amount }
3844
- }
3845
- );
3846
- settlement.status = SETTLEMENT_STATUS.PROCESSING;
3847
- settlement.processedAt = /* @__PURE__ */ new Date();
3848
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3849
- await settlement.save();
3850
- result.succeeded++;
3851
- result.settlements.push(settlement);
3852
- this.events.emit("settlement.processing", {
3853
- settlement,
3854
- processedAt: settlement.processedAt
3855
- });
3856
- } catch (error) {
3857
- result.failed++;
3858
- result.errors.push({
3859
- settlementId: settlement._id.toString(),
3860
- error: error.message
3861
- });
3862
- this.logger.error("Failed to process settlement", {
3863
- settlementId: settlement._id,
3864
- error
3865
- });
3866
- }
3867
- }
3868
- this.logger.info("Processed settlements", result);
3869
- return result;
3870
- }
3871
- /**
3872
- * Mark settlement as completed
3873
- * Call this after bank confirms the transfer
3874
- *
3875
- * @param settlementId - Settlement ID
3876
- * @param details - Completion details
3877
- * @returns Updated settlement
3878
- */
3879
- async complete(settlementId, details = {}) {
3880
- if (!this.models.Settlement) {
3881
- throw new ModelNotRegisteredError("Settlement");
3882
- }
3883
- const SettlementModel = this.models.Settlement;
3884
- const settlement = await SettlementModel.findById(settlementId);
3885
- if (!settlement) {
3886
- throw new ValidationError("Settlement not found", { settlementId });
3887
- }
3888
- if (settlement.status !== SETTLEMENT_STATUS.PROCESSING && settlement.status !== SETTLEMENT_STATUS.PENDING) {
3889
- throw new InvalidStateTransitionError(
3890
- "complete",
3891
- SETTLEMENT_STATUS.PROCESSING,
3892
- settlement.status,
3893
- "Only processing or pending settlements can be completed"
3894
- );
3895
- }
3896
- const {
3897
- transferReference,
3898
- transferredAt = /* @__PURE__ */ new Date(),
3899
- transactionHash,
3900
- notes,
3901
- metadata = {}
3902
- } = details;
3903
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3904
- settlement.status,
3905
- SETTLEMENT_STATUS.COMPLETED,
3906
- settlement._id.toString(),
3907
- {
3908
- changedBy: "system",
3909
- reason: "Settlement completed successfully",
3910
- metadata: {
3911
- transferReference,
3912
- transferredAt,
3913
- transactionHash,
3914
- payoutMethod: settlement.payoutMethod,
3915
- amount: settlement.amount
3916
- }
3917
- }
3918
- );
3919
- settlement.status = SETTLEMENT_STATUS.COMPLETED;
3920
- settlement.completedAt = /* @__PURE__ */ new Date();
3921
- if (settlement.payoutMethod === "bank_transfer" && transferReference) {
3922
- settlement.bankTransferDetails = {
3923
- ...settlement.bankTransferDetails,
3924
- transferReference,
3925
- transferredAt
3926
- };
3927
- } else if (settlement.payoutMethod === "crypto" && transactionHash) {
3928
- settlement.cryptoDetails = {
3929
- ...settlement.cryptoDetails,
3930
- transactionHash,
3931
- transferredAt
3932
- };
3933
- } else if (settlement.payoutMethod === "mobile_wallet") {
3934
- settlement.mobileWalletDetails = {
3935
- ...settlement.mobileWalletDetails,
3936
- transferredAt
3937
- };
3938
- } else if (settlement.payoutMethod === "platform_balance") {
3939
- settlement.platformBalanceDetails = {
3940
- ...settlement.platformBalanceDetails,
3941
- appliedAt: transferredAt
3942
- };
3943
- }
3944
- if (notes) settlement.notes = notes;
3945
- settlement.metadata = { ...settlement.metadata, ...metadata };
3946
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3947
- await settlement.save();
3948
- this.events.emit("settlement.completed", {
3949
- settlement,
3950
- completedAt: settlement.completedAt
3951
- });
3952
- this.logger.info("Settlement completed", {
3953
- settlementId: settlement._id,
3954
- recipientId: settlement.recipientId,
3955
- amount: settlement.amount
3956
- });
3957
- return settlement;
3958
- }
3959
- /**
3960
- * Mark settlement as failed
3961
- *
3962
- * @param settlementId - Settlement ID
3963
- * @param reason - Failure reason
3964
- * @returns Updated settlement
3965
- */
3966
- async fail(settlementId, reason, options = {}) {
3967
- if (!this.models.Settlement) {
3968
- throw new ModelNotRegisteredError("Settlement");
3969
- }
3970
- const SettlementModel = this.models.Settlement;
3971
- const settlement = await SettlementModel.findById(settlementId);
3972
- if (!settlement) {
3973
- throw new ValidationError("Settlement not found", { settlementId });
3974
- }
3975
- const { code, retry: retry2 = false } = options;
3976
- if (retry2) {
3977
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3978
- settlement.status,
3979
- SETTLEMENT_STATUS.PENDING,
3980
- settlement._id.toString(),
3981
- {
3982
- changedBy: "system",
3983
- reason: `Settlement failed, retrying: ${reason}`,
3984
- metadata: {
3985
- failureReason: reason,
3986
- failureCode: code,
3987
- retryCount: (settlement.retryCount || 0) + 1,
3988
- scheduledAt: new Date(Date.now() + 60 * 60 * 1e3)
3989
- }
3990
- }
3991
- );
3992
- settlement.status = SETTLEMENT_STATUS.PENDING;
3993
- settlement.retryCount = (settlement.retryCount || 0) + 1;
3994
- settlement.scheduledAt = new Date(Date.now() + 60 * 60 * 1e3);
3995
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3996
- } else {
3997
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3998
- settlement.status,
3999
- SETTLEMENT_STATUS.FAILED,
4000
- settlement._id.toString(),
4001
- {
4002
- changedBy: "system",
4003
- reason: `Settlement failed: ${reason}`,
4004
- metadata: {
4005
- failureReason: reason,
4006
- failureCode: code
4007
- }
4008
- }
4009
- );
4010
- settlement.status = SETTLEMENT_STATUS.FAILED;
4011
- settlement.failedAt = /* @__PURE__ */ new Date();
4012
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
4013
- }
4014
- settlement.failureReason = reason;
4015
- if (code) settlement.failureCode = code;
4016
- await settlement.save();
4017
- this.events.emit("settlement.failed", {
4018
- settlement,
4019
- reason,
4020
- code,
4021
- retry: retry2
4022
- });
4023
- this.logger.warn("Settlement failed", {
4024
- settlementId: settlement._id,
4025
- reason,
4026
- retry: retry2
4027
- });
4028
- return settlement;
4029
- }
4030
- /**
4031
- * List settlements with filters
4032
- *
4033
- * @param filters - Query filters
4034
- * @returns Settlements
4035
- */
4036
- async list(filters = {}) {
4037
- if (!this.models.Settlement) {
4038
- throw new ModelNotRegisteredError("Settlement");
4039
- }
4040
- const SettlementModel = this.models.Settlement;
4041
- const {
4042
- organizationId,
4043
- recipientId,
4044
- status,
4045
- type,
4046
- payoutMethod,
4047
- scheduledAfter,
4048
- scheduledBefore,
4049
- limit = 50,
4050
- skip = 0,
4051
- sort = { createdAt: -1 }
4052
- } = filters;
4053
- const query = {};
4054
- if (organizationId) query.organizationId = organizationId;
4055
- if (recipientId) query.recipientId = recipientId;
4056
- if (status) query.status = Array.isArray(status) ? { $in: status } : status;
4057
- if (type) query.type = type;
4058
- if (payoutMethod) query.payoutMethod = payoutMethod;
4059
- if (scheduledAfter || scheduledBefore) {
4060
- query.scheduledAt = {};
4061
- if (scheduledAfter) query.scheduledAt.$gte = scheduledAfter;
4062
- if (scheduledBefore) query.scheduledAt.$lte = scheduledBefore;
4063
- }
4064
- const settlements = await SettlementModel.find(query).limit(limit).skip(skip).sort(sort);
4065
- return settlements;
4066
- }
4067
- /**
4068
- * Get payout summary for recipient
4069
- *
4070
- * @param recipientId - Recipient ID
4071
- * @param options - Summary options
4072
- * @returns Settlement summary
4073
- */
4074
- async getSummary(recipientId, options = {}) {
4075
- if (!this.models.Settlement) {
4076
- throw new ModelNotRegisteredError("Settlement");
4077
- }
4078
- const { organizationId, startDate, endDate } = options;
4079
- const SettlementModel = this.models.Settlement;
4080
- const query = { recipientId };
4081
- if (organizationId) query.organizationId = organizationId;
4082
- if (startDate || endDate) {
4083
- query.createdAt = {};
4084
- if (startDate) query.createdAt.$gte = startDate;
4085
- if (endDate) query.createdAt.$lte = endDate;
4086
- }
4087
- const settlements = await SettlementModel.find(query);
4088
- const summary = {
4089
- recipientId,
4090
- totalPending: 0,
4091
- totalProcessing: 0,
4092
- totalCompleted: 0,
4093
- totalFailed: 0,
4094
- amountPending: 0,
4095
- amountCompleted: 0,
4096
- amountFailed: 0,
4097
- currency: settlements[0]?.currency || "USD",
4098
- settlements: {
4099
- pending: 0,
4100
- processing: 0,
4101
- completed: 0,
4102
- failed: 0,
4103
- cancelled: 0
4104
- }
4105
- };
4106
- for (const settlement of settlements) {
4107
- summary.settlements[settlement.status]++;
4108
- if (settlement.status === SETTLEMENT_STATUS.PENDING) {
4109
- summary.totalPending++;
4110
- summary.amountPending += settlement.amount;
4111
- } else if (settlement.status === SETTLEMENT_STATUS.PROCESSING) {
4112
- summary.totalProcessing++;
4113
- } else if (settlement.status === SETTLEMENT_STATUS.COMPLETED) {
4114
- summary.totalCompleted++;
4115
- summary.amountCompleted += settlement.amount;
4116
- if (!summary.lastSettlementDate || settlement.completedAt > summary.lastSettlementDate) {
4117
- summary.lastSettlementDate = settlement.completedAt;
4118
- }
4119
- } else if (settlement.status === SETTLEMENT_STATUS.FAILED) {
4120
- summary.totalFailed++;
4121
- summary.amountFailed += settlement.amount;
4122
- }
4123
- }
4124
- return summary;
4125
- }
4126
- /**
4127
- * Get settlement by ID
4128
- *
4129
- * @param settlementId - Settlement ID
4130
- * @returns Settlement
4131
- */
4132
- async get(settlementId) {
4133
- if (!this.models.Settlement) {
4134
- throw new ModelNotRegisteredError("Settlement");
4135
- }
4136
- const SettlementModel = this.models.Settlement;
4137
- const settlement = await SettlementModel.findById(settlementId);
4138
- if (!settlement) {
4139
- throw new ValidationError("Settlement not found", { settlementId });
4140
- }
4141
- return settlement;
4142
- }
4143
- };
4144
-
4145
- // src/core/revenue.ts
4146
- var Revenue = class {
4147
- // ============ CORE ============
4148
- _container;
4149
- _events;
4150
- _plugins;
4151
- _idempotency;
4152
- _circuitBreaker;
4153
- _options;
4154
- _logger;
4155
- _providers;
4156
- _config;
4157
- // ============ SERVICES ============
4158
- /** Monetization service - purchases, subscriptions, free items */
4159
- monetization;
4160
- /** Payment service - verify, refund, webhooks */
4161
- payments;
4162
- /** Transaction service - query, update transactions */
4163
- transactions;
4164
- /** Escrow service - hold, release, splits */
4165
- escrow;
4166
- /** Settlement service - payout tracking */
4167
- settlement;
4168
- constructor(container, events, plugins, options, providers, config) {
4169
- this._container = container;
4170
- this._events = events;
4171
- this._plugins = plugins;
4172
- this._options = options;
4173
- this._logger = options.logger;
4174
- this._providers = providers;
4175
- this._config = config;
4176
- this._idempotency = createIdempotencyManager({
4177
- ttl: options.idempotencyTtl
4178
- });
4179
- if (options.circuitBreaker) {
4180
- const cbConfig = typeof options.circuitBreaker === "boolean" ? {} : options.circuitBreaker;
4181
- this._circuitBreaker = createCircuitBreaker(cbConfig);
4182
- }
4183
- container.singleton("events", events);
4184
- container.singleton("plugins", plugins);
4185
- container.singleton("idempotency", this._idempotency);
4186
- container.singleton("logger", this._logger);
4187
- container.singleton("retryConfig", options.retry || {});
4188
- container.singleton("circuitBreaker", this._circuitBreaker || null);
4189
- this.monetization = new MonetizationService(container);
4190
- this.payments = new PaymentService(container);
4191
- this.transactions = new TransactionService(container);
4192
- this.escrow = new EscrowService(container);
4193
- this.settlement = new SettlementService(container);
4194
- Object.freeze(this._providers);
4195
- Object.freeze(this._config);
4196
- }
4197
- // ============ STATIC FACTORY ============
4198
- /**
4199
- * Create a new Revenue builder
4200
- *
4201
- * @example
4202
- * ```typescript
4203
- * const revenue = Revenue
4204
- * .create({ defaultCurrency: 'BDT' })
4205
- * .withModels({ Transaction, Subscription })
4206
- * .withProvider('manual', new ManualProvider())
4207
- * .build();
4208
- * ```
4209
- */
4210
- static create(options = {}) {
4211
- return new RevenueBuilder(options);
4212
- }
4213
- // ============ ACCESSORS ============
4214
- /** DI container (for advanced usage) */
4215
- get container() {
4216
- return this._container;
4217
- }
4218
- /** Event bus */
4219
- get events() {
4220
- return this._events;
4221
- }
4222
- /** Registered providers (frozen) */
4223
- get providers() {
4224
- return this._providers;
4225
- }
4226
- /** Configuration (frozen) */
4227
- get config() {
4228
- return this._config;
4229
- }
4230
- /** Default currency */
4231
- get defaultCurrency() {
4232
- return this._options.defaultCurrency;
4233
- }
4234
- /** Current environment */
4235
- get environment() {
4236
- return this._options.environment;
4237
- }
4238
- // ============ PROVIDER METHODS ============
4239
- /**
4240
- * Get a provider by name
4241
- */
4242
- getProvider(name) {
4243
- const provider = this._providers[name];
4244
- if (!provider) {
4245
- throw new ConfigurationError(
4246
- `Provider "${name}" not found. Available: ${Object.keys(this._providers).join(", ")}`
4247
- );
4248
- }
4249
- return provider;
4250
- }
4251
- /**
4252
- * Get all provider names
4253
- */
4254
- getProviderNames() {
4255
- return Object.keys(this._providers);
4256
- }
4257
- /**
4258
- * Check if provider exists
4259
- */
4260
- hasProvider(name) {
4261
- return name in this._providers;
4262
- }
4263
- // ============ EVENT SYSTEM ============
4264
- /**
4265
- * Subscribe to events
4266
- *
4267
- * @example
4268
- * ```typescript
4269
- * revenue.on('payment.verified', (event) => {
4270
- * console.log('Payment:', event.transaction._id);
4271
- * });
4272
- * ```
4273
- */
4274
- on = (event, handler) => {
4275
- return this._events.on(event, handler);
4276
- };
4277
- /**
4278
- * Subscribe once
4279
- */
4280
- once = (event, handler) => {
4281
- return this._events.once(event, handler);
4282
- };
4283
- /**
4284
- * Unsubscribe
4285
- */
4286
- off = (event, handler) => {
4287
- this._events.off(event, handler);
4288
- };
4289
- /**
4290
- * Emit an event
4291
- */
4292
- emit = (event, payload) => {
4293
- this._events.emit(event, payload);
4294
- };
4295
- // ============ RESILIENCE ============
4296
- /**
4297
- * Execute operation with retry and idempotency
4298
- */
4299
- async execute(operation, options = {}) {
4300
- const { idempotencyKey, params, useRetry = true, useCircuitBreaker = true } = options;
4301
- const idempotentOp = async () => {
4302
- if (idempotencyKey) {
4303
- const result = await this._idempotency.execute(idempotencyKey, params, operation);
4304
- if (!result.ok) throw result.error;
4305
- return result.value;
4306
- }
4307
- return operation();
4308
- };
4309
- const resilientOp = async () => {
4310
- if (useCircuitBreaker && this._circuitBreaker) {
4311
- return this._circuitBreaker.execute(idempotentOp);
4312
- }
4313
- return idempotentOp();
4314
- };
4315
- if (useRetry && this._options.retry) {
4316
- return tryCatch(() => retry(resilientOp, this._options.retry));
4317
- }
4318
- return tryCatch(resilientOp);
4319
- }
4320
- /**
4321
- * Create plugin context (for advanced usage)
4322
- */
4323
- createContext(meta = {}) {
4324
- return {
4325
- events: this._events,
4326
- logger: this._logger,
4327
- storage: /* @__PURE__ */ new Map(),
4328
- meta: {
4329
- ...meta,
4330
- requestId: nanoid(12),
4331
- timestamp: /* @__PURE__ */ new Date()
4332
- }
4333
- };
4334
- }
4335
- /**
4336
- * Destroy instance and cleanup
4337
- */
4338
- async destroy() {
4339
- await this._plugins.destroy();
4340
- this._events.clear();
4341
- this._idempotency.destroy();
4342
- }
4343
- };
4344
- var RevenueBuilder = class {
4345
- options;
4346
- models = null;
4347
- providers = {};
4348
- plugins = [];
4349
- categoryMappings = {};
4350
- transactionTypeMapping = {};
4351
- constructor(options = {}) {
4352
- this.options = options;
4353
- }
4354
- /**
4355
- * Register models (required)
4356
- *
4357
- * @example
4358
- * ```typescript
4359
- * .withModels({
4360
- * Transaction: TransactionModel,
4361
- * Subscription: SubscriptionModel,
4362
- * })
4363
- * ```
4364
- */
4365
- withModels(models) {
4366
- this.models = models;
4367
- return this;
4368
- }
4369
- /**
4370
- * Register a single model
4371
- */
4372
- withModel(name, model) {
4373
- if (!this.models) {
4374
- this.models = {};
4375
- }
4376
- this.models[name] = model;
4377
- return this;
4378
- }
4379
- /**
4380
- * Register a payment provider
4381
- *
4382
- * @example
4383
- * ```typescript
4384
- * .withProvider('stripe', new StripeProvider({ apiKey: '...' }))
4385
- * .withProvider('manual', new ManualProvider())
4386
- * ```
4387
- */
4388
- withProvider(name, provider) {
4389
- this.providers[name] = provider;
4390
- return this;
4391
- }
4392
- /**
4393
- * Register multiple providers at once
4394
- */
4395
- withProviders(providers) {
4396
- this.providers = { ...this.providers, ...providers };
4397
- return this;
4398
- }
4399
- /**
4400
- * Register a plugin
4401
- *
4402
- * @example
4403
- * ```typescript
4404
- * .withPlugin(loggingPlugin())
4405
- * .withPlugin(auditPlugin({ store: saveToDb }))
4406
- * ```
4407
- */
4408
- withPlugin(plugin) {
4409
- this.plugins.push(plugin);
4410
- return this;
4411
- }
4412
- /**
4413
- * Register multiple plugins
4414
- */
4415
- withPlugins(plugins) {
4416
- this.plugins.push(...plugins);
4417
- return this;
4418
- }
4419
- /**
4420
- * Set retry configuration
4421
- *
4422
- * @example
4423
- * ```typescript
4424
- * .withRetry({ maxAttempts: 5, baseDelay: 2000 })
4425
- * ```
4426
- */
4427
- withRetry(config) {
4428
- this.options.retry = config;
4429
- return this;
4430
- }
4431
- /**
4432
- * Enable circuit breaker
4433
- */
4434
- withCircuitBreaker(enabled = true) {
4435
- this.options.circuitBreaker = enabled;
4436
- return this;
4437
- }
4438
- /**
4439
- * Set custom logger
4440
- */
4441
- withLogger(logger) {
4442
- this.options.logger = logger;
4443
- return this;
4444
- }
4445
- /**
4446
- * Set environment
4447
- */
4448
- forEnvironment(env) {
4449
- this.options.environment = env;
4450
- return this;
4451
- }
4452
- /**
4453
- * Enable debug mode
4454
- */
4455
- withDebug(enabled = true) {
4456
- this.options.debug = enabled;
4457
- return this;
4458
- }
4459
- /**
4460
- * Set commission rate as decimal (0-1, e.g., 0.10 for 10%)
4461
- *
4462
- * @param rate - Commission rate (0-1 decimal format)
4463
- * @param gatewayFeeRate - Gateway fee rate (0-1 decimal format, e.g., 0.029 for 2.9%)
4464
- *
4465
- * @example
4466
- * ```typescript
4467
- * .withCommission(0.10, 0.029) // 10% commission + 2.9% gateway fee
4468
- * ```
4469
- */
4470
- withCommission(rate, gatewayFeeRate = 0) {
4471
- this.options.commissionRate = rate;
4472
- this.options.gatewayFeeRate = gatewayFeeRate;
4473
- return this;
4474
- }
4475
- /**
4476
- * Set category mappings (entity → category)
4477
- *
4478
- * @example
4479
- * ```typescript
4480
- * .withCategoryMappings({
4481
- * PlatformSubscription: 'platform_subscription',
4482
- * CourseEnrollment: 'course_enrollment',
4483
- * ProductOrder: 'product_order',
4484
- * })
4485
- * ```
4486
- */
4487
- withCategoryMappings(mappings) {
4488
- this.categoryMappings = { ...this.categoryMappings, ...mappings };
4489
- return this;
4490
- }
4491
- /**
4492
- * Set transaction flow mapping by category or monetization type
4493
- *
4494
- * @example
4495
- * ```typescript
4496
- * .withTransactionTypeMapping({
4497
- * platform_subscription: 'inflow',
4498
- * subscription: 'inflow',
4499
- * refund: 'outflow',
4500
- * })
4501
- * ```
4502
- */
4503
- withTransactionTypeMapping(mapping) {
4504
- this.transactionTypeMapping = { ...this.transactionTypeMapping, ...mapping };
4505
- return this;
4506
- }
4507
- /**
4508
- * Build the Revenue instance
4509
- */
4510
- build() {
4511
- if (!this.models) {
4512
- throw new ConfigurationError(
4513
- "Models are required. Use .withModels({ Transaction, Subscription })"
4514
- );
4515
- }
4516
- if (!this.models.Transaction) {
4517
- throw new ConfigurationError(
4518
- "Transaction model is required in models configuration"
4519
- );
4520
- }
4521
- if (Object.keys(this.providers).length === 0) {
4522
- throw new ConfigurationError(
4523
- "At least one provider is required. Use .withProvider(name, provider)"
4524
- );
4525
- }
4526
- const container = new Container();
4527
- const defaultLogger = {
4528
- debug: (msg, data) => this.options.debug && console.debug(`[Revenue] ${msg}`, data ?? ""),
4529
- info: (msg, data) => console.info(`[Revenue] ${msg}`, data ?? ""),
4530
- warn: (msg, data) => console.warn(`[Revenue] ${msg}`, data ?? ""),
4531
- error: (msg, data) => console.error(`[Revenue] ${msg}`, data ?? "")
4532
- };
4533
- const resolvedOptions = {
4534
- defaultCurrency: this.options.defaultCurrency ?? "USD",
4535
- environment: this.options.environment ?? "development",
4536
- debug: this.options.debug ?? false,
4537
- retry: this.options.retry ?? { maxAttempts: 3 },
4538
- idempotencyTtl: this.options.idempotencyTtl ?? 24 * 60 * 60 * 1e3,
4539
- circuitBreaker: this.options.circuitBreaker ?? false,
4540
- logger: this.options.logger ?? defaultLogger,
4541
- commissionRate: this.options.commissionRate ?? 0,
4542
- gatewayFeeRate: this.options.gatewayFeeRate ?? 0
4543
- };
4544
- const config = {
4545
- ...resolveConfig(resolvedOptions),
4546
- // Converts singular → plural with '*' global default
4547
- targetModels: [],
4548
- categoryMappings: this.categoryMappings,
4549
- transactionTypeMapping: this.transactionTypeMapping
4550
- };
4551
- for (const provider of Object.values(this.providers)) {
4552
- if (typeof provider.setDefaultCurrency === "function") {
4553
- provider.setDefaultCurrency(resolvedOptions.defaultCurrency);
4554
- }
4555
- }
4556
- container.singleton("models", this.models);
4557
- container.singleton("providers", this.providers);
4558
- container.singleton("config", config);
4559
- const events = createEventBus();
4560
- const pluginManager = new PluginManager();
4561
- for (const plugin of this.plugins) {
4562
- pluginManager.register(plugin);
4563
- }
4564
- const revenue = new Revenue(
4565
- container,
4566
- events,
4567
- pluginManager,
4568
- resolvedOptions,
4569
- this.providers,
4570
- config
4571
- );
4572
- const ctx = revenue.createContext();
4573
- pluginManager.init(ctx).catch((err2) => {
4574
- resolvedOptions.logger.error("Failed to initialize plugins", err2);
4575
- });
4576
- return revenue;
4577
- }
4578
- };
4579
- function createRevenue(config) {
4580
- let builder = Revenue.create(config.options);
4581
- builder = builder.withModels(config.models);
4582
- builder = builder.withProviders(config.providers);
4583
- if (config.plugins) {
4584
- builder = builder.withPlugins(config.plugins);
4585
- }
4586
- return builder.build();
4587
- }
4588
-
4589
- export { AlreadyVerifiedError, ConfigurationError, Container, ERROR_CODES, EventBus, HOLD_STATE_MACHINE, InvalidAmountError, InvalidStateTransitionError, MissingRequiredFieldError, ModelNotRegisteredError, NotFoundError, OperationError, PaymentIntentCreationError, PaymentVerificationError, PluginManager, ProviderCapabilityError, ProviderError, ProviderNotFoundError, RefundError, RefundNotSupportedError, Result, Revenue, RevenueBuilder, RevenueError, SETTLEMENT_STATE_MACHINE, SPLIT_STATE_MACHINE, SUBSCRIPTION_STATE_MACHINE, StateError, StateMachine, SubscriptionNotActiveError, SubscriptionNotFoundError, TRANSACTION_STATE_MACHINE, TransactionNotFoundError, ValidationError, all, auditPlugin, createEventBus, createRevenue, definePlugin, err, flatMap, isErr, isOk, isRetryable, isRevenueError, loggingPlugin, map, mapErr, match, metricsPlugin, ok, tryCatch, tryCatchSync, unwrap, unwrapOr };
4590
- //# sourceMappingURL=index.js.map
4591
- //# sourceMappingURL=index.js.map