@classytic/revenue 1.1.2 → 1.1.4

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