@classytic/revenue 0.2.4 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +498 -501
  2. package/dist/actions-CwG-b7fR.d.ts +519 -0
  3. package/dist/core/index.d.ts +884 -0
  4. package/dist/core/index.js +2941 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/enums/index.d.ts +130 -0
  7. package/dist/enums/index.js +167 -0
  8. package/dist/enums/index.js.map +1 -0
  9. package/dist/index-BnJWVXuw.d.ts +378 -0
  10. package/dist/index-ChVD3P9k.d.ts +504 -0
  11. package/dist/index.d.ts +42 -0
  12. package/dist/index.js +4362 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/providers/index.d.ts +132 -0
  15. package/dist/providers/index.js +122 -0
  16. package/dist/providers/index.js.map +1 -0
  17. package/dist/retry-80lBCmSe.d.ts +234 -0
  18. package/dist/schemas/index.d.ts +906 -0
  19. package/dist/schemas/index.js +533 -0
  20. package/dist/schemas/index.js.map +1 -0
  21. package/dist/schemas/validation.d.ts +309 -0
  22. package/dist/schemas/validation.js +249 -0
  23. package/dist/schemas/validation.js.map +1 -0
  24. package/dist/services/index.d.ts +3 -0
  25. package/dist/services/index.js +1632 -0
  26. package/dist/services/index.js.map +1 -0
  27. package/dist/split.enums-DHdM1YAV.d.ts +255 -0
  28. package/dist/split.schema-CETjPq10.d.ts +976 -0
  29. package/dist/utils/index.d.ts +24 -0
  30. package/dist/utils/index.js +1067 -0
  31. package/dist/utils/index.js.map +1 -0
  32. package/package.json +48 -32
  33. package/core/builder.js +0 -219
  34. package/core/container.js +0 -119
  35. package/core/errors.js +0 -262
  36. package/dist/types/core/builder.d.ts +0 -97
  37. package/dist/types/core/container.d.ts +0 -57
  38. package/dist/types/core/errors.d.ts +0 -122
  39. package/dist/types/enums/escrow.enums.d.ts +0 -24
  40. package/dist/types/enums/index.d.ts +0 -69
  41. package/dist/types/enums/monetization.enums.d.ts +0 -6
  42. package/dist/types/enums/payment.enums.d.ts +0 -16
  43. package/dist/types/enums/split.enums.d.ts +0 -25
  44. package/dist/types/enums/subscription.enums.d.ts +0 -15
  45. package/dist/types/enums/transaction.enums.d.ts +0 -24
  46. package/dist/types/index.d.ts +0 -22
  47. package/dist/types/providers/base.d.ts +0 -128
  48. package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
  49. package/dist/types/schemas/escrow/index.d.ts +0 -6
  50. package/dist/types/schemas/index.d.ts +0 -506
  51. package/dist/types/schemas/split/index.d.ts +0 -8
  52. package/dist/types/schemas/split/split.schema.d.ts +0 -142
  53. package/dist/types/schemas/subscription/index.d.ts +0 -152
  54. package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
  55. package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
  56. package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
  57. package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
  58. package/dist/types/schemas/transaction/index.d.ts +0 -202
  59. package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
  60. package/dist/types/services/escrow.service.d.ts +0 -51
  61. package/dist/types/services/monetization.service.d.ts +0 -193
  62. package/dist/types/services/payment.service.d.ts +0 -117
  63. package/dist/types/services/transaction.service.d.ts +0 -40
  64. package/dist/types/utils/category-resolver.d.ts +0 -46
  65. package/dist/types/utils/commission-split.d.ts +0 -56
  66. package/dist/types/utils/commission.d.ts +0 -29
  67. package/dist/types/utils/hooks.d.ts +0 -17
  68. package/dist/types/utils/index.d.ts +0 -6
  69. package/dist/types/utils/logger.d.ts +0 -12
  70. package/dist/types/utils/subscription/actions.d.ts +0 -28
  71. package/dist/types/utils/subscription/index.d.ts +0 -2
  72. package/dist/types/utils/subscription/period.d.ts +0 -47
  73. package/dist/types/utils/transaction-type.d.ts +0 -102
  74. package/enums/escrow.enums.js +0 -36
  75. package/enums/index.d.ts +0 -116
  76. package/enums/index.js +0 -110
  77. package/enums/monetization.enums.js +0 -15
  78. package/enums/payment.enums.js +0 -64
  79. package/enums/split.enums.js +0 -37
  80. package/enums/subscription.enums.js +0 -33
  81. package/enums/transaction.enums.js +0 -69
  82. package/index.js +0 -76
  83. package/providers/base.js +0 -162
  84. package/schemas/escrow/hold.schema.js +0 -62
  85. package/schemas/escrow/index.js +0 -15
  86. package/schemas/index.d.ts +0 -33
  87. package/schemas/index.js +0 -27
  88. package/schemas/split/index.js +0 -16
  89. package/schemas/split/split.schema.js +0 -86
  90. package/schemas/subscription/index.js +0 -17
  91. package/schemas/subscription/info.schema.js +0 -115
  92. package/schemas/subscription/plan.schema.js +0 -48
  93. package/schemas/transaction/common.schema.js +0 -22
  94. package/schemas/transaction/gateway.schema.js +0 -69
  95. package/schemas/transaction/index.js +0 -20
  96. package/schemas/transaction/payment.schema.js +0 -110
  97. package/services/escrow.service.js +0 -353
  98. package/services/monetization.service.js +0 -675
  99. package/services/payment.service.js +0 -535
  100. package/services/transaction.service.js +0 -142
  101. package/utils/category-resolver.js +0 -74
  102. package/utils/commission-split.js +0 -180
  103. package/utils/commission.js +0 -83
  104. package/utils/hooks.js +0 -44
  105. package/utils/index.d.ts +0 -164
  106. package/utils/index.js +0 -16
  107. package/utils/logger.js +0 -36
  108. package/utils/subscription/actions.js +0 -68
  109. package/utils/subscription/index.js +0 -20
  110. package/utils/subscription/period.js +0 -123
  111. package/utils/transaction-type.js +0 -254
@@ -0,0 +1,2941 @@
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
+ /**
136
+ * Emit an event (fire and forget, non-blocking)
137
+ */
138
+ emit(event, payload) {
139
+ const fullPayload = {
140
+ ...payload,
141
+ timestamp: /* @__PURE__ */ new Date()
142
+ };
143
+ const handlers = this.handlers.get(event);
144
+ if (handlers) {
145
+ for (const handler of handlers) {
146
+ Promise.resolve(handler(fullPayload)).catch((err2) => {
147
+ console.error(`[Revenue] Event handler error for "${event}":`, err2);
148
+ });
149
+ }
150
+ }
151
+ const onceHandlers = this.onceHandlers.get(event);
152
+ if (onceHandlers) {
153
+ for (const handler of onceHandlers) {
154
+ Promise.resolve(handler(fullPayload)).catch((err2) => {
155
+ console.error(`[Revenue] Once handler error for "${event}":`, err2);
156
+ });
157
+ }
158
+ this.onceHandlers.delete(event);
159
+ }
160
+ if (event !== "*") {
161
+ const wildcardHandlers = this.handlers.get("*");
162
+ if (wildcardHandlers) {
163
+ for (const handler of wildcardHandlers) {
164
+ Promise.resolve(handler(fullPayload)).catch((err2) => {
165
+ console.error(`[Revenue] Wildcard handler error:`, err2);
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * Emit and wait for all handlers to complete
173
+ */
174
+ async emitAsync(event, payload) {
175
+ const fullPayload = {
176
+ ...payload,
177
+ timestamp: /* @__PURE__ */ new Date()
178
+ };
179
+ const promises = [];
180
+ const handlers = this.handlers.get(event);
181
+ if (handlers) {
182
+ for (const handler of handlers) {
183
+ promises.push(Promise.resolve(handler(fullPayload)));
184
+ }
185
+ }
186
+ const onceHandlers = this.onceHandlers.get(event);
187
+ if (onceHandlers) {
188
+ for (const handler of onceHandlers) {
189
+ promises.push(Promise.resolve(handler(fullPayload)));
190
+ }
191
+ this.onceHandlers.delete(event);
192
+ }
193
+ if (event !== "*") {
194
+ const wildcardHandlers = this.handlers.get("*");
195
+ if (wildcardHandlers) {
196
+ for (const handler of wildcardHandlers) {
197
+ promises.push(Promise.resolve(handler(fullPayload)));
198
+ }
199
+ }
200
+ }
201
+ await Promise.all(promises);
202
+ }
203
+ /**
204
+ * Remove all handlers
205
+ */
206
+ clear() {
207
+ this.handlers.clear();
208
+ this.onceHandlers.clear();
209
+ }
210
+ /**
211
+ * Get handler count for an event
212
+ */
213
+ listenerCount(event) {
214
+ return (this.handlers.get(event)?.size ?? 0) + (this.onceHandlers.get(event)?.size ?? 0);
215
+ }
216
+ };
217
+ function createEventBus() {
218
+ return new EventBus();
219
+ }
220
+
221
+ // src/core/plugin.ts
222
+ var PluginManager = class {
223
+ plugins = /* @__PURE__ */ new Map();
224
+ hooks = /* @__PURE__ */ new Map();
225
+ initialized = false;
226
+ /**
227
+ * Register a plugin
228
+ */
229
+ register(plugin) {
230
+ if (this.plugins.has(plugin.name)) {
231
+ throw new Error(`Plugin "${plugin.name}" is already registered`);
232
+ }
233
+ if (plugin.dependencies) {
234
+ for (const dep of plugin.dependencies) {
235
+ if (!this.plugins.has(dep)) {
236
+ throw new Error(
237
+ `Plugin "${plugin.name}" requires "${dep}" to be registered first`
238
+ );
239
+ }
240
+ }
241
+ }
242
+ this.plugins.set(plugin.name, plugin);
243
+ if (plugin.hooks) {
244
+ for (const [hookName, hookFn] of Object.entries(plugin.hooks)) {
245
+ if (!this.hooks.has(hookName)) {
246
+ this.hooks.set(hookName, []);
247
+ }
248
+ this.hooks.get(hookName).push(hookFn);
249
+ }
250
+ }
251
+ return this;
252
+ }
253
+ /**
254
+ * Initialize all plugins
255
+ */
256
+ async init(ctx) {
257
+ if (this.initialized) return;
258
+ for (const plugin of this.plugins.values()) {
259
+ if (plugin.init) {
260
+ await plugin.init(ctx);
261
+ }
262
+ if (plugin.events) {
263
+ for (const [event, handler] of Object.entries(plugin.events)) {
264
+ ctx.events.on(event, handler);
265
+ }
266
+ }
267
+ }
268
+ this.initialized = true;
269
+ }
270
+ /**
271
+ * Execute a hook chain
272
+ */
273
+ async executeHook(hookName, ctx, input, execute) {
274
+ const hooks = this.hooks.get(hookName) ?? [];
275
+ if (hooks.length === 0) {
276
+ return execute();
277
+ }
278
+ let index = 0;
279
+ const next = async () => {
280
+ if (index >= hooks.length) {
281
+ return execute();
282
+ }
283
+ const hook = hooks[index++];
284
+ return hook(ctx, input, next);
285
+ };
286
+ return next();
287
+ }
288
+ /**
289
+ * Check if plugin is registered
290
+ */
291
+ has(name) {
292
+ return this.plugins.has(name);
293
+ }
294
+ /**
295
+ * Get a plugin by name
296
+ */
297
+ get(name) {
298
+ return this.plugins.get(name);
299
+ }
300
+ /**
301
+ * Get all registered plugins
302
+ */
303
+ list() {
304
+ return Array.from(this.plugins.values());
305
+ }
306
+ /**
307
+ * Destroy all plugins
308
+ */
309
+ async destroy() {
310
+ for (const plugin of this.plugins.values()) {
311
+ if (plugin.destroy) {
312
+ await plugin.destroy();
313
+ }
314
+ }
315
+ this.plugins.clear();
316
+ this.hooks.clear();
317
+ this.initialized = false;
318
+ }
319
+ };
320
+ function loggingPlugin(options = {}) {
321
+ const level = options.level ?? "info";
322
+ return {
323
+ name: "logging",
324
+ version: "1.0.0",
325
+ description: "Logs all revenue operations",
326
+ hooks: {
327
+ "payment.create.before": async (ctx, input, next) => {
328
+ ctx.logger[level]("Creating payment", { amount: input.amount, currency: input.currency });
329
+ const result = await next();
330
+ ctx.logger[level]("Payment created", { transactionId: result?.transactionId });
331
+ return result;
332
+ },
333
+ "payment.verify.before": async (ctx, input, next) => {
334
+ ctx.logger[level]("Verifying payment", { id: input.id });
335
+ const result = await next();
336
+ ctx.logger[level]("Payment verified", { verified: result?.verified });
337
+ return result;
338
+ },
339
+ "payment.refund.before": async (ctx, input, next) => {
340
+ ctx.logger[level]("Processing refund", { transactionId: input.transactionId, amount: input.amount });
341
+ const result = await next();
342
+ ctx.logger[level]("Refund processed", { refundId: result?.refundId });
343
+ return result;
344
+ }
345
+ }
346
+ };
347
+ }
348
+ function auditPlugin(options = {}) {
349
+ const store = options.store ?? (async (entry) => {
350
+ });
351
+ return {
352
+ name: "audit",
353
+ version: "1.0.0",
354
+ description: "Audit trail for all operations",
355
+ hooks: {
356
+ "payment.create.after": async (ctx, input, next) => {
357
+ const result = await next();
358
+ await store({
359
+ action: "payment.create",
360
+ requestId: ctx.meta.requestId,
361
+ timestamp: ctx.meta.timestamp,
362
+ input: sanitizeInput(input),
363
+ output: sanitizeOutput(result),
364
+ idempotencyKey: ctx.meta.idempotencyKey
365
+ });
366
+ return result;
367
+ },
368
+ "payment.refund.after": async (ctx, input, next) => {
369
+ const result = await next();
370
+ await store({
371
+ action: "payment.refund",
372
+ requestId: ctx.meta.requestId,
373
+ timestamp: ctx.meta.timestamp,
374
+ input: sanitizeInput(input),
375
+ output: sanitizeOutput(result),
376
+ idempotencyKey: ctx.meta.idempotencyKey
377
+ });
378
+ return result;
379
+ }
380
+ }
381
+ };
382
+ }
383
+ function sanitizeInput(input) {
384
+ if (typeof input !== "object" || !input) return {};
385
+ const sanitized = { ...input };
386
+ delete sanitized.apiKey;
387
+ delete sanitized.secretKey;
388
+ delete sanitized.password;
389
+ return sanitized;
390
+ }
391
+ function sanitizeOutput(output) {
392
+ if (typeof output !== "object" || !output) return {};
393
+ return { ...output };
394
+ }
395
+ function metricsPlugin(options = {}) {
396
+ const record = options.onMetric ?? ((metric) => {
397
+ });
398
+ return {
399
+ name: "metrics",
400
+ version: "1.0.0",
401
+ description: "Collects operation metrics",
402
+ hooks: {
403
+ "payment.create.before": async (_ctx, input, next) => {
404
+ const start = Date.now();
405
+ try {
406
+ const result = await next();
407
+ record({
408
+ name: "payment.create",
409
+ duration: Date.now() - start,
410
+ success: true,
411
+ amount: input.amount,
412
+ currency: input.currency
413
+ });
414
+ return result;
415
+ } catch (error) {
416
+ record({
417
+ name: "payment.create",
418
+ duration: Date.now() - start,
419
+ success: false,
420
+ error: error.message
421
+ });
422
+ throw error;
423
+ }
424
+ }
425
+ }
426
+ };
427
+ }
428
+ function definePlugin(plugin) {
429
+ return plugin;
430
+ }
431
+
432
+ // src/core/result.ts
433
+ function ok(value) {
434
+ return { ok: true, value };
435
+ }
436
+ function err(error) {
437
+ return { ok: false, error };
438
+ }
439
+ function isOk(result) {
440
+ return result.ok === true;
441
+ }
442
+ function isErr(result) {
443
+ return result.ok === false;
444
+ }
445
+ function unwrap(result) {
446
+ if (result.ok) return result.value;
447
+ throw result.error;
448
+ }
449
+ function unwrapOr(result, defaultValue) {
450
+ return result.ok ? result.value : defaultValue;
451
+ }
452
+ function map(result, fn) {
453
+ return result.ok ? ok(fn(result.value)) : result;
454
+ }
455
+ function mapErr(result, fn) {
456
+ return result.ok ? result : err(fn(result.error));
457
+ }
458
+ function flatMap(result, fn) {
459
+ return result.ok ? fn(result.value) : result;
460
+ }
461
+ async function tryCatch(fn, mapError) {
462
+ try {
463
+ const value = await fn();
464
+ return ok(value);
465
+ } catch (e) {
466
+ const error = mapError ? mapError(e) : e;
467
+ return err(error);
468
+ }
469
+ }
470
+ function tryCatchSync(fn, mapError) {
471
+ try {
472
+ const value = fn();
473
+ return ok(value);
474
+ } catch (e) {
475
+ const error = mapError ? mapError(e) : e;
476
+ return err(error);
477
+ }
478
+ }
479
+ function all(results) {
480
+ const values = [];
481
+ for (const result of results) {
482
+ if (!result.ok) return result;
483
+ values.push(result.value);
484
+ }
485
+ return ok(values);
486
+ }
487
+ function match(result, handlers) {
488
+ return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
489
+ }
490
+ var Result = {
491
+ ok,
492
+ err,
493
+ isOk,
494
+ isErr,
495
+ unwrap,
496
+ unwrapOr,
497
+ map,
498
+ mapErr,
499
+ flatMap,
500
+ tryCatch,
501
+ tryCatchSync,
502
+ all,
503
+ match
504
+ };
505
+ var MemoryIdempotencyStore = class {
506
+ records = /* @__PURE__ */ new Map();
507
+ cleanupInterval = null;
508
+ constructor(cleanupIntervalMs = 6e4) {
509
+ this.cleanupInterval = setInterval(() => {
510
+ this.cleanup();
511
+ }, cleanupIntervalMs);
512
+ }
513
+ async get(key) {
514
+ const record = this.records.get(key);
515
+ if (!record) return null;
516
+ if (record.expiresAt < /* @__PURE__ */ new Date()) {
517
+ this.records.delete(key);
518
+ return null;
519
+ }
520
+ return record;
521
+ }
522
+ async set(key, record) {
523
+ this.records.set(key, record);
524
+ }
525
+ async delete(key) {
526
+ this.records.delete(key);
527
+ }
528
+ async exists(key) {
529
+ const record = await this.get(key);
530
+ return record !== null;
531
+ }
532
+ cleanup() {
533
+ const now = /* @__PURE__ */ new Date();
534
+ for (const [key, record] of this.records) {
535
+ if (record.expiresAt < now) {
536
+ this.records.delete(key);
537
+ }
538
+ }
539
+ }
540
+ destroy() {
541
+ if (this.cleanupInterval) {
542
+ clearInterval(this.cleanupInterval);
543
+ this.cleanupInterval = null;
544
+ }
545
+ this.records.clear();
546
+ }
547
+ };
548
+ var IdempotencyError = class extends Error {
549
+ constructor(message, code) {
550
+ super(message);
551
+ this.code = code;
552
+ this.name = "IdempotencyError";
553
+ }
554
+ };
555
+ var IdempotencyManager = class {
556
+ store;
557
+ ttl;
558
+ prefix;
559
+ constructor(config = {}) {
560
+ this.store = config.store ?? new MemoryIdempotencyStore();
561
+ this.ttl = config.ttl ?? 24 * 60 * 60 * 1e3;
562
+ this.prefix = config.prefix ?? "idem:";
563
+ }
564
+ /**
565
+ * Generate a unique idempotency key
566
+ */
567
+ generateKey() {
568
+ return `${this.prefix}${nanoid(21)}`;
569
+ }
570
+ /**
571
+ * Hash request parameters for validation
572
+ */
573
+ hashRequest(params) {
574
+ const json = JSON.stringify(params, Object.keys(params).sort());
575
+ let hash = 0;
576
+ for (let i = 0; i < json.length; i++) {
577
+ const char = json.charCodeAt(i);
578
+ hash = (hash << 5) - hash + char;
579
+ hash = hash & hash;
580
+ }
581
+ return hash.toString(36);
582
+ }
583
+ /**
584
+ * Execute operation with idempotency protection
585
+ */
586
+ async execute(key, params, operation) {
587
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
588
+ const requestHash = this.hashRequest(params);
589
+ const existing = await this.store.get(fullKey);
590
+ if (existing) {
591
+ if (existing.requestHash !== requestHash) {
592
+ return err(new IdempotencyError(
593
+ "Idempotency key used with different request parameters",
594
+ "REQUEST_MISMATCH"
595
+ ));
596
+ }
597
+ if (existing.status === "completed" && existing.result !== void 0) {
598
+ return ok(existing.result);
599
+ }
600
+ if (existing.status === "pending") {
601
+ return err(new IdempotencyError(
602
+ "Request with this idempotency key is already in progress",
603
+ "REQUEST_IN_PROGRESS"
604
+ ));
605
+ }
606
+ if (existing.status === "failed") {
607
+ await this.store.delete(fullKey);
608
+ }
609
+ }
610
+ const record = {
611
+ key: fullKey,
612
+ status: "pending",
613
+ createdAt: /* @__PURE__ */ new Date(),
614
+ requestHash,
615
+ expiresAt: new Date(Date.now() + this.ttl)
616
+ };
617
+ await this.store.set(fullKey, record);
618
+ try {
619
+ const result = await operation();
620
+ record.status = "completed";
621
+ record.result = result;
622
+ record.completedAt = /* @__PURE__ */ new Date();
623
+ await this.store.set(fullKey, record);
624
+ return ok(result);
625
+ } catch (error) {
626
+ record.status = "failed";
627
+ await this.store.set(fullKey, record);
628
+ throw error;
629
+ }
630
+ }
631
+ /**
632
+ * Check if operation with key was already completed
633
+ */
634
+ async wasCompleted(key) {
635
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
636
+ const record = await this.store.get(fullKey);
637
+ return record?.status === "completed";
638
+ }
639
+ /**
640
+ * Get cached result for key
641
+ */
642
+ async getCached(key) {
643
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
644
+ const record = await this.store.get(fullKey);
645
+ return record?.status === "completed" ? record.result ?? null : null;
646
+ }
647
+ /**
648
+ * Invalidate a key (force re-execution on next call)
649
+ */
650
+ async invalidate(key) {
651
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
652
+ await this.store.delete(fullKey);
653
+ }
654
+ };
655
+ function createIdempotencyManager(config) {
656
+ return new IdempotencyManager(config);
657
+ }
658
+
659
+ // src/utils/retry.ts
660
+ var DEFAULT_CONFIG = {
661
+ maxAttempts: 3,
662
+ baseDelay: 1e3,
663
+ maxDelay: 3e4,
664
+ backoffMultiplier: 2,
665
+ jitter: 0.1,
666
+ retryIf: isRetryableError
667
+ };
668
+ function calculateDelay(attempt, config) {
669
+ const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
670
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
671
+ const jitterRange = cappedDelay * config.jitter;
672
+ const jitter = Math.random() * jitterRange * 2 - jitterRange;
673
+ return Math.round(Math.max(0, cappedDelay + jitter));
674
+ }
675
+ function sleep(ms) {
676
+ return new Promise((resolve) => setTimeout(resolve, ms));
677
+ }
678
+ function isRetryableError(error) {
679
+ if (!(error instanceof Error)) return false;
680
+ if (error.message.includes("ECONNREFUSED")) return true;
681
+ if (error.message.includes("ETIMEDOUT")) return true;
682
+ if (error.message.includes("ENOTFOUND")) return true;
683
+ if (error.message.includes("network")) return true;
684
+ if (error.message.includes("timeout")) return true;
685
+ if (error.message.includes("429")) return true;
686
+ if (error.message.includes("rate limit")) return true;
687
+ if (error.message.includes("500")) return true;
688
+ if (error.message.includes("502")) return true;
689
+ if (error.message.includes("503")) return true;
690
+ if (error.message.includes("504")) return true;
691
+ if ("retryable" in error && error.retryable === true) return true;
692
+ return false;
693
+ }
694
+ async function retry(operation, config = {}) {
695
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
696
+ const state = {
697
+ attempt: 0,
698
+ totalDelay: 0,
699
+ errors: []
700
+ };
701
+ while (state.attempt < fullConfig.maxAttempts) {
702
+ try {
703
+ return await operation();
704
+ } catch (error) {
705
+ state.errors.push(error instanceof Error ? error : new Error(String(error)));
706
+ state.attempt++;
707
+ const shouldRetry = fullConfig.retryIf?.(error) ?? isRetryableError(error);
708
+ if (!shouldRetry || state.attempt >= fullConfig.maxAttempts) {
709
+ throw new RetryExhaustedError(
710
+ `Operation failed after ${state.attempt} attempts`,
711
+ state.errors
712
+ );
713
+ }
714
+ const delay = calculateDelay(state.attempt - 1, fullConfig);
715
+ state.totalDelay += delay;
716
+ fullConfig.onRetry?.(error, state.attempt, delay);
717
+ await sleep(delay);
718
+ }
719
+ }
720
+ throw new RetryExhaustedError(
721
+ `Operation failed after ${state.attempt} attempts`,
722
+ state.errors
723
+ );
724
+ }
725
+ var RetryExhaustedError = class extends Error {
726
+ attempts;
727
+ errors;
728
+ constructor(message, errors) {
729
+ super(message);
730
+ this.name = "RetryExhaustedError";
731
+ this.attempts = errors.length;
732
+ this.errors = errors;
733
+ }
734
+ /**
735
+ * Get the last error
736
+ */
737
+ get lastError() {
738
+ return this.errors[this.errors.length - 1];
739
+ }
740
+ /**
741
+ * Get the first error
742
+ */
743
+ get firstError() {
744
+ return this.errors[0];
745
+ }
746
+ };
747
+ var DEFAULT_CIRCUIT_CONFIG = {
748
+ failureThreshold: 5,
749
+ resetTimeout: 3e4,
750
+ successThreshold: 3,
751
+ monitorWindow: 6e4
752
+ };
753
+ var CircuitBreaker = class {
754
+ state = "closed";
755
+ failures = [];
756
+ successes = 0;
757
+ lastFailure;
758
+ config;
759
+ constructor(config = {}) {
760
+ this.config = { ...DEFAULT_CIRCUIT_CONFIG, ...config };
761
+ }
762
+ /**
763
+ * Execute operation through circuit breaker
764
+ */
765
+ async execute(operation) {
766
+ if (this.state === "open") {
767
+ if (this.shouldAttemptReset()) {
768
+ this.state = "half-open";
769
+ } else {
770
+ throw new CircuitOpenError("Circuit is open, request rejected");
771
+ }
772
+ }
773
+ try {
774
+ const result = await operation();
775
+ this.onSuccess();
776
+ return result;
777
+ } catch (error) {
778
+ this.onFailure();
779
+ throw error;
780
+ }
781
+ }
782
+ /**
783
+ * Execute with Result type
784
+ */
785
+ async executeWithResult(operation) {
786
+ try {
787
+ const result = await this.execute(operation);
788
+ return ok(result);
789
+ } catch (error) {
790
+ return err(error);
791
+ }
792
+ }
793
+ onSuccess() {
794
+ if (this.state === "half-open") {
795
+ this.successes++;
796
+ if (this.successes >= this.config.successThreshold) {
797
+ this.reset();
798
+ }
799
+ }
800
+ this.cleanOldFailures();
801
+ }
802
+ onFailure() {
803
+ this.failures.push(/* @__PURE__ */ new Date());
804
+ this.lastFailure = /* @__PURE__ */ new Date();
805
+ this.successes = 0;
806
+ this.cleanOldFailures();
807
+ if (this.failures.length >= this.config.failureThreshold) {
808
+ this.state = "open";
809
+ }
810
+ }
811
+ shouldAttemptReset() {
812
+ if (!this.lastFailure) return true;
813
+ return Date.now() - this.lastFailure.getTime() >= this.config.resetTimeout;
814
+ }
815
+ cleanOldFailures() {
816
+ const cutoff = Date.now() - this.config.monitorWindow;
817
+ this.failures = this.failures.filter((f) => f.getTime() > cutoff);
818
+ }
819
+ reset() {
820
+ this.state = "closed";
821
+ this.failures = [];
822
+ this.successes = 0;
823
+ }
824
+ /**
825
+ * Get current circuit state
826
+ */
827
+ getState() {
828
+ return this.state;
829
+ }
830
+ /**
831
+ * Manually reset circuit
832
+ */
833
+ forceReset() {
834
+ this.reset();
835
+ }
836
+ /**
837
+ * Get circuit statistics
838
+ */
839
+ getStats() {
840
+ return {
841
+ state: this.state,
842
+ failures: this.failures.length,
843
+ successes: this.successes,
844
+ lastFailure: this.lastFailure
845
+ };
846
+ }
847
+ };
848
+ var CircuitOpenError = class extends Error {
849
+ constructor(message) {
850
+ super(message);
851
+ this.name = "CircuitOpenError";
852
+ }
853
+ };
854
+ function createCircuitBreaker(config) {
855
+ return new CircuitBreaker(config);
856
+ }
857
+
858
+ // src/core/errors.ts
859
+ var RevenueError = class extends Error {
860
+ code;
861
+ retryable;
862
+ metadata;
863
+ constructor(message, code, options = {}) {
864
+ super(message);
865
+ this.name = this.constructor.name;
866
+ this.code = code;
867
+ this.retryable = options.retryable ?? false;
868
+ this.metadata = options.metadata ?? {};
869
+ Error.captureStackTrace(this, this.constructor);
870
+ }
871
+ toJSON() {
872
+ return {
873
+ name: this.name,
874
+ message: this.message,
875
+ code: this.code,
876
+ retryable: this.retryable,
877
+ metadata: this.metadata
878
+ };
879
+ }
880
+ };
881
+ var ConfigurationError = class extends RevenueError {
882
+ constructor(message, metadata = {}) {
883
+ super(message, "CONFIGURATION_ERROR", { retryable: false, metadata });
884
+ }
885
+ };
886
+ var ModelNotRegisteredError = class extends ConfigurationError {
887
+ constructor(modelName) {
888
+ super(
889
+ `Model "${modelName}" is not registered. Register it via createRevenue({ models: { ${modelName}: ... } })`,
890
+ { modelName }
891
+ );
892
+ }
893
+ };
894
+ var ProviderError = class extends RevenueError {
895
+ constructor(message, code, options = {}) {
896
+ super(message, code, options);
897
+ }
898
+ };
899
+ var ProviderNotFoundError = class extends ProviderError {
900
+ constructor(providerName, availableProviders = []) {
901
+ super(
902
+ `Payment provider "${providerName}" not found. Available: ${availableProviders.join(", ")}`,
903
+ "PROVIDER_NOT_FOUND",
904
+ { retryable: false, metadata: { providerName, availableProviders } }
905
+ );
906
+ }
907
+ };
908
+ var ProviderCapabilityError = class extends ProviderError {
909
+ constructor(providerName, capability) {
910
+ super(
911
+ `Provider "${providerName}" does not support ${capability}`,
912
+ "PROVIDER_CAPABILITY_NOT_SUPPORTED",
913
+ { retryable: false, metadata: { providerName, capability } }
914
+ );
915
+ }
916
+ };
917
+ var PaymentIntentCreationError = class extends ProviderError {
918
+ constructor(providerName, originalError) {
919
+ super(
920
+ `Failed to create payment intent with provider "${providerName}": ${originalError.message}`,
921
+ "PAYMENT_INTENT_CREATION_FAILED",
922
+ { retryable: true, metadata: { providerName, originalError: originalError.message } }
923
+ );
924
+ }
925
+ };
926
+ var PaymentVerificationError = class extends ProviderError {
927
+ constructor(paymentIntentId, reason) {
928
+ super(
929
+ `Payment verification failed for intent "${paymentIntentId}": ${reason}`,
930
+ "PAYMENT_VERIFICATION_FAILED",
931
+ { retryable: true, metadata: { paymentIntentId, reason } }
932
+ );
933
+ }
934
+ };
935
+ var NotFoundError = class extends RevenueError {
936
+ constructor(message, code, metadata = {}) {
937
+ super(message, code, { retryable: false, metadata });
938
+ }
939
+ };
940
+ var SubscriptionNotFoundError = class extends NotFoundError {
941
+ constructor(subscriptionId) {
942
+ super(
943
+ `Subscription not found: ${subscriptionId}`,
944
+ "SUBSCRIPTION_NOT_FOUND",
945
+ { subscriptionId }
946
+ );
947
+ }
948
+ };
949
+ var TransactionNotFoundError = class extends NotFoundError {
950
+ constructor(transactionId) {
951
+ super(
952
+ `Transaction not found: ${transactionId}`,
953
+ "TRANSACTION_NOT_FOUND",
954
+ { transactionId }
955
+ );
956
+ }
957
+ };
958
+ var ValidationError = class extends RevenueError {
959
+ constructor(message, metadata = {}) {
960
+ super(message, "VALIDATION_ERROR", { retryable: false, metadata });
961
+ }
962
+ };
963
+ var InvalidAmountError = class extends ValidationError {
964
+ constructor(amount, message) {
965
+ super(
966
+ message ?? `Invalid amount: ${amount}. Amount must be non-negative`,
967
+ { amount }
968
+ );
969
+ }
970
+ };
971
+ var MissingRequiredFieldError = class extends ValidationError {
972
+ constructor(fieldName) {
973
+ super(`Missing required field: ${fieldName}`, { fieldName });
974
+ }
975
+ };
976
+ var StateError = class extends RevenueError {
977
+ constructor(message, code, metadata = {}) {
978
+ super(message, code, { retryable: false, metadata });
979
+ }
980
+ };
981
+ var AlreadyVerifiedError = class extends StateError {
982
+ constructor(transactionId) {
983
+ super(
984
+ `Transaction ${transactionId} is already verified`,
985
+ "ALREADY_VERIFIED",
986
+ { transactionId }
987
+ );
988
+ }
989
+ };
990
+ var InvalidStateTransitionError = class extends StateError {
991
+ constructor(resourceType, resourceId, fromState, toState) {
992
+ super(
993
+ `Invalid state transition for ${resourceType} ${resourceId}: ${fromState} \u2192 ${toState}`,
994
+ "INVALID_STATE_TRANSITION",
995
+ { resourceType, resourceId, fromState, toState }
996
+ );
997
+ }
998
+ };
999
+ var SubscriptionNotActiveError = class extends StateError {
1000
+ constructor(subscriptionId, message) {
1001
+ super(
1002
+ message ?? `Subscription ${subscriptionId} is not active`,
1003
+ "SUBSCRIPTION_NOT_ACTIVE",
1004
+ { subscriptionId }
1005
+ );
1006
+ }
1007
+ };
1008
+ var OperationError = class extends RevenueError {
1009
+ constructor(message, code, options = {}) {
1010
+ super(message, code, options);
1011
+ }
1012
+ };
1013
+ var RefundNotSupportedError = class extends OperationError {
1014
+ constructor(providerName) {
1015
+ super(
1016
+ `Refunds are not supported by provider "${providerName}"`,
1017
+ "REFUND_NOT_SUPPORTED",
1018
+ { retryable: false, metadata: { providerName } }
1019
+ );
1020
+ }
1021
+ };
1022
+ var RefundError = class extends OperationError {
1023
+ constructor(transactionId, reason) {
1024
+ super(
1025
+ `Refund failed for transaction ${transactionId}: ${reason}`,
1026
+ "REFUND_FAILED",
1027
+ { retryable: true, metadata: { transactionId, reason } }
1028
+ );
1029
+ }
1030
+ };
1031
+ var ERROR_CODES = {
1032
+ // Configuration
1033
+ CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
1034
+ MODEL_NOT_REGISTERED: "MODEL_NOT_REGISTERED",
1035
+ // Provider
1036
+ PROVIDER_NOT_FOUND: "PROVIDER_NOT_FOUND",
1037
+ PROVIDER_CAPABILITY_NOT_SUPPORTED: "PROVIDER_CAPABILITY_NOT_SUPPORTED",
1038
+ PAYMENT_INTENT_CREATION_FAILED: "PAYMENT_INTENT_CREATION_FAILED",
1039
+ PAYMENT_VERIFICATION_FAILED: "PAYMENT_VERIFICATION_FAILED",
1040
+ // Not Found
1041
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
1042
+ TRANSACTION_NOT_FOUND: "TRANSACTION_NOT_FOUND",
1043
+ // Validation
1044
+ VALIDATION_ERROR: "VALIDATION_ERROR",
1045
+ INVALID_AMOUNT: "INVALID_AMOUNT",
1046
+ MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
1047
+ // State
1048
+ ALREADY_VERIFIED: "ALREADY_VERIFIED",
1049
+ INVALID_STATE_TRANSITION: "INVALID_STATE_TRANSITION",
1050
+ SUBSCRIPTION_NOT_ACTIVE: "SUBSCRIPTION_NOT_ACTIVE",
1051
+ // Operations
1052
+ REFUND_NOT_SUPPORTED: "REFUND_NOT_SUPPORTED",
1053
+ REFUND_FAILED: "REFUND_FAILED"
1054
+ };
1055
+ function isRetryable(error) {
1056
+ return error instanceof RevenueError && error.retryable;
1057
+ }
1058
+ function isRevenueError(error) {
1059
+ return error instanceof RevenueError;
1060
+ }
1061
+
1062
+ // src/utils/hooks.ts
1063
+ function triggerHook(hooks, event, data, logger) {
1064
+ const handlers = hooks[event] ?? [];
1065
+ if (handlers.length === 0) {
1066
+ return;
1067
+ }
1068
+ Promise.all(
1069
+ handlers.map(
1070
+ (handler) => Promise.resolve(handler(data)).catch((error) => {
1071
+ logger.error(`Hook "${event}" failed:`, {
1072
+ error: error.message,
1073
+ stack: error.stack,
1074
+ event,
1075
+ // Don't log full data (could be huge)
1076
+ dataKeys: Object.keys(data)
1077
+ });
1078
+ })
1079
+ )
1080
+ ).catch(() => {
1081
+ });
1082
+ }
1083
+
1084
+ // src/enums/transaction.enums.ts
1085
+ var TRANSACTION_TYPE = {
1086
+ INCOME: "income",
1087
+ EXPENSE: "expense"
1088
+ };
1089
+ var TRANSACTION_STATUS = {
1090
+ VERIFIED: "verified",
1091
+ COMPLETED: "completed",
1092
+ CANCELLED: "cancelled"};
1093
+ var LIBRARY_CATEGORIES = {
1094
+ SUBSCRIPTION: "subscription",
1095
+ PURCHASE: "purchase"
1096
+ };
1097
+
1098
+ // src/utils/category-resolver.ts
1099
+ function resolveCategory(entity, monetizationType, categoryMappings = {}) {
1100
+ if (entity && categoryMappings[entity]) {
1101
+ return categoryMappings[entity];
1102
+ }
1103
+ switch (monetizationType) {
1104
+ case "subscription":
1105
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
1106
+ // 'subscription'
1107
+ case "purchase":
1108
+ return LIBRARY_CATEGORIES.PURCHASE;
1109
+ // 'purchase'
1110
+ default:
1111
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
1112
+ }
1113
+ }
1114
+
1115
+ // src/utils/commission.ts
1116
+ function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
1117
+ if (!commissionRate || commissionRate <= 0) {
1118
+ return null;
1119
+ }
1120
+ if (amount < 0) {
1121
+ throw new Error("Transaction amount cannot be negative");
1122
+ }
1123
+ if (commissionRate < 0 || commissionRate > 1) {
1124
+ throw new Error("Commission rate must be between 0 and 1");
1125
+ }
1126
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
1127
+ throw new Error("Gateway fee rate must be between 0 and 1");
1128
+ }
1129
+ const grossAmount = Math.round(amount * commissionRate * 100) / 100;
1130
+ const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
1131
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
1132
+ return {
1133
+ rate: commissionRate,
1134
+ grossAmount,
1135
+ gatewayFeeRate,
1136
+ gatewayFeeAmount,
1137
+ netAmount,
1138
+ status: "pending"
1139
+ };
1140
+ }
1141
+ function reverseCommission(originalCommission, originalAmount, refundAmount) {
1142
+ if (!originalCommission?.netAmount) {
1143
+ return null;
1144
+ }
1145
+ const refundRatio = refundAmount / originalAmount;
1146
+ const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
1147
+ const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
1148
+ const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
1149
+ return {
1150
+ rate: originalCommission.rate,
1151
+ grossAmount: reversedGrossAmount,
1152
+ gatewayFeeRate: originalCommission.gatewayFeeRate,
1153
+ gatewayFeeAmount: reversedGatewayFee,
1154
+ netAmount: reversedNetAmount,
1155
+ status: "waived"
1156
+ // Commission waived due to refund
1157
+ };
1158
+ }
1159
+
1160
+ // src/enums/monetization.enums.ts
1161
+ var MONETIZATION_TYPES = {
1162
+ FREE: "free",
1163
+ PURCHASE: "purchase",
1164
+ SUBSCRIPTION: "subscription"
1165
+ };
1166
+
1167
+ // src/services/monetization.service.ts
1168
+ var MonetizationService = class {
1169
+ models;
1170
+ providers;
1171
+ config;
1172
+ hooks;
1173
+ logger;
1174
+ constructor(container) {
1175
+ this.models = container.get("models");
1176
+ this.providers = container.get("providers");
1177
+ this.config = container.get("config");
1178
+ this.hooks = container.get("hooks");
1179
+ this.logger = container.get("logger");
1180
+ }
1181
+ /**
1182
+ * Create a new monetization (purchase, subscription, or free item)
1183
+ *
1184
+ * @param params - Monetization parameters
1185
+ *
1186
+ * @example
1187
+ * // One-time purchase
1188
+ * await revenue.monetization.create({
1189
+ * data: {
1190
+ * organizationId: '...',
1191
+ * customerId: '...',
1192
+ * referenceId: order._id,
1193
+ * referenceModel: 'Order',
1194
+ * },
1195
+ * planKey: 'one_time',
1196
+ * monetizationType: 'purchase',
1197
+ * gateway: 'bkash',
1198
+ * amount: 1500,
1199
+ * });
1200
+ *
1201
+ * // Recurring subscription
1202
+ * await revenue.monetization.create({
1203
+ * data: {
1204
+ * organizationId: '...',
1205
+ * customerId: '...',
1206
+ * referenceId: subscription._id,
1207
+ * referenceModel: 'Subscription',
1208
+ * },
1209
+ * planKey: 'monthly',
1210
+ * monetizationType: 'subscription',
1211
+ * gateway: 'stripe',
1212
+ * amount: 2000,
1213
+ * });
1214
+ *
1215
+ * @returns Result with subscription, transaction, and paymentIntent
1216
+ */
1217
+ async create(params) {
1218
+ const {
1219
+ data,
1220
+ planKey,
1221
+ amount,
1222
+ currency = "BDT",
1223
+ gateway = "manual",
1224
+ entity = null,
1225
+ monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
1226
+ paymentData,
1227
+ metadata = {},
1228
+ idempotencyKey = null
1229
+ } = params;
1230
+ if (!planKey) {
1231
+ throw new MissingRequiredFieldError("planKey");
1232
+ }
1233
+ if (amount < 0) {
1234
+ throw new InvalidAmountError(amount);
1235
+ }
1236
+ const isFree = amount === 0;
1237
+ const provider = this.providers[gateway];
1238
+ if (!provider) {
1239
+ throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1240
+ }
1241
+ let paymentIntent = null;
1242
+ let transaction = null;
1243
+ if (!isFree) {
1244
+ try {
1245
+ paymentIntent = await provider.createIntent({
1246
+ amount,
1247
+ currency,
1248
+ metadata: {
1249
+ ...metadata,
1250
+ type: "subscription",
1251
+ planKey
1252
+ }
1253
+ });
1254
+ } catch (error) {
1255
+ throw new PaymentIntentCreationError(gateway, error);
1256
+ }
1257
+ const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
1258
+ const transactionType = this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_TYPE.INCOME;
1259
+ const commissionRate = this.config.commissionRates?.[category] ?? 0;
1260
+ const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
1261
+ const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
1262
+ const TransactionModel = this.models.Transaction;
1263
+ transaction = await TransactionModel.create({
1264
+ organizationId: data.organizationId,
1265
+ customerId: data.customerId ?? null,
1266
+ amount,
1267
+ currency,
1268
+ category,
1269
+ type: transactionType,
1270
+ method: paymentData?.method ?? "manual",
1271
+ status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1272
+ gateway: {
1273
+ type: gateway,
1274
+ sessionId: paymentIntent.sessionId,
1275
+ paymentIntentId: paymentIntent.paymentIntentId,
1276
+ provider: paymentIntent.provider,
1277
+ metadata: paymentIntent.metadata
1278
+ },
1279
+ paymentDetails: {
1280
+ provider: gateway,
1281
+ ...paymentData
1282
+ },
1283
+ ...commission && { commission },
1284
+ // Only include if commission exists
1285
+ // Polymorphic reference (top-level, not metadata)
1286
+ ...data.referenceId && { referenceId: data.referenceId },
1287
+ ...data.referenceModel && { referenceModel: data.referenceModel },
1288
+ metadata: {
1289
+ ...metadata,
1290
+ planKey,
1291
+ entity,
1292
+ monetizationType,
1293
+ paymentIntentId: paymentIntent.id
1294
+ },
1295
+ idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
1296
+ });
1297
+ }
1298
+ let subscription = null;
1299
+ if (this.models.Subscription) {
1300
+ const SubscriptionModel = this.models.Subscription;
1301
+ const subscriptionData = {
1302
+ organizationId: data.organizationId,
1303
+ customerId: data.customerId ?? null,
1304
+ planKey,
1305
+ amount,
1306
+ currency,
1307
+ status: isFree ? "active" : "pending",
1308
+ isActive: isFree,
1309
+ gateway,
1310
+ transactionId: transaction?._id ?? null,
1311
+ paymentIntentId: paymentIntent?.id ?? null,
1312
+ metadata: {
1313
+ ...metadata,
1314
+ isFree,
1315
+ entity,
1316
+ monetizationType
1317
+ },
1318
+ ...data
1319
+ };
1320
+ delete subscriptionData.referenceId;
1321
+ delete subscriptionData.referenceModel;
1322
+ subscription = await SubscriptionModel.create(subscriptionData);
1323
+ }
1324
+ const eventData = {
1325
+ subscription,
1326
+ transaction,
1327
+ paymentIntent,
1328
+ isFree,
1329
+ monetizationType
1330
+ };
1331
+ if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
1332
+ this._triggerHook("purchase.created", eventData);
1333
+ } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
1334
+ this._triggerHook("subscription.created", eventData);
1335
+ } else if (monetizationType === MONETIZATION_TYPES.FREE) {
1336
+ this._triggerHook("free.created", eventData);
1337
+ }
1338
+ this._triggerHook("monetization.created", eventData);
1339
+ return {
1340
+ subscription,
1341
+ transaction,
1342
+ paymentIntent
1343
+ };
1344
+ }
1345
+ /**
1346
+ * Activate subscription after payment verification
1347
+ *
1348
+ * @param subscriptionId - Subscription ID or transaction ID
1349
+ * @param options - Activation options
1350
+ * @returns Updated subscription
1351
+ */
1352
+ async activate(subscriptionId, options = {}) {
1353
+ const { timestamp = /* @__PURE__ */ new Date() } = options;
1354
+ if (!this.models.Subscription) {
1355
+ throw new ModelNotRegisteredError("Subscription");
1356
+ }
1357
+ const SubscriptionModel = this.models.Subscription;
1358
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1359
+ if (!subscription) {
1360
+ throw new SubscriptionNotFoundError(subscriptionId);
1361
+ }
1362
+ if (subscription.isActive) {
1363
+ this.logger.warn("Subscription already active", { subscriptionId });
1364
+ return subscription;
1365
+ }
1366
+ const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
1367
+ subscription.isActive = true;
1368
+ subscription.status = "active";
1369
+ subscription.startDate = timestamp;
1370
+ subscription.endDate = periodEnd;
1371
+ subscription.activatedAt = timestamp;
1372
+ await subscription.save();
1373
+ this._triggerHook("subscription.activated", {
1374
+ subscription,
1375
+ activatedAt: timestamp
1376
+ });
1377
+ return subscription;
1378
+ }
1379
+ /**
1380
+ * Renew subscription
1381
+ *
1382
+ * @param subscriptionId - Subscription ID
1383
+ * @param params - Renewal parameters
1384
+ * @returns { subscription, transaction, paymentIntent }
1385
+ */
1386
+ async renew(subscriptionId, params = {}) {
1387
+ const {
1388
+ gateway = "manual",
1389
+ entity = null,
1390
+ paymentData,
1391
+ metadata = {},
1392
+ idempotencyKey = null
1393
+ } = params;
1394
+ if (!this.models.Subscription) {
1395
+ throw new ModelNotRegisteredError("Subscription");
1396
+ }
1397
+ const SubscriptionModel = this.models.Subscription;
1398
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1399
+ if (!subscription) {
1400
+ throw new SubscriptionNotFoundError(subscriptionId);
1401
+ }
1402
+ if (subscription.amount === 0) {
1403
+ throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
1404
+ }
1405
+ const provider = this.providers[gateway];
1406
+ if (!provider) {
1407
+ throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1408
+ }
1409
+ let paymentIntent = null;
1410
+ try {
1411
+ paymentIntent = await provider.createIntent({
1412
+ amount: subscription.amount,
1413
+ currency: subscription.currency ?? "BDT",
1414
+ metadata: {
1415
+ ...metadata,
1416
+ type: "subscription_renewal",
1417
+ subscriptionId: subscription._id.toString()
1418
+ }
1419
+ });
1420
+ } catch (error) {
1421
+ this.logger.error("Failed to create payment intent for renewal:", error);
1422
+ throw new PaymentIntentCreationError(gateway, error);
1423
+ }
1424
+ const effectiveEntity = entity ?? subscription.metadata?.entity;
1425
+ const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
1426
+ const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
1427
+ const transactionType = this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_TYPE.INCOME;
1428
+ const commissionRate = this.config.commissionRates?.[category] ?? 0;
1429
+ const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
1430
+ const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
1431
+ const TransactionModel = this.models.Transaction;
1432
+ const transaction = await TransactionModel.create({
1433
+ organizationId: subscription.organizationId,
1434
+ customerId: subscription.customerId,
1435
+ amount: subscription.amount,
1436
+ currency: subscription.currency ?? "BDT",
1437
+ category,
1438
+ type: transactionType,
1439
+ method: paymentData?.method ?? "manual",
1440
+ status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1441
+ gateway: {
1442
+ type: gateway,
1443
+ sessionId: paymentIntent.sessionId,
1444
+ paymentIntentId: paymentIntent.paymentIntentId,
1445
+ provider: paymentIntent.provider,
1446
+ metadata: paymentIntent.metadata
1447
+ },
1448
+ paymentDetails: {
1449
+ provider: gateway,
1450
+ ...paymentData
1451
+ },
1452
+ ...commission && { commission },
1453
+ // Only include if commission exists
1454
+ // Polymorphic reference to subscription
1455
+ referenceId: subscription._id,
1456
+ referenceModel: "Subscription",
1457
+ metadata: {
1458
+ ...metadata,
1459
+ subscriptionId: subscription._id.toString(),
1460
+ // Keep for backward compat
1461
+ entity: effectiveEntity,
1462
+ monetizationType: effectiveMonetizationType,
1463
+ isRenewal: true,
1464
+ paymentIntentId: paymentIntent.id
1465
+ },
1466
+ idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
1467
+ });
1468
+ subscription.status = "pending_renewal";
1469
+ subscription.renewalTransactionId = transaction._id;
1470
+ subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
1471
+ await subscription.save();
1472
+ this._triggerHook("subscription.renewed", {
1473
+ subscription,
1474
+ transaction,
1475
+ paymentIntent,
1476
+ renewalCount: subscription.renewalCount
1477
+ });
1478
+ return {
1479
+ subscription,
1480
+ transaction,
1481
+ paymentIntent
1482
+ };
1483
+ }
1484
+ /**
1485
+ * Cancel subscription
1486
+ *
1487
+ * @param subscriptionId - Subscription ID
1488
+ * @param options - Cancellation options
1489
+ * @returns Updated subscription
1490
+ */
1491
+ async cancel(subscriptionId, options = {}) {
1492
+ const { immediate = false, reason = null } = options;
1493
+ if (!this.models.Subscription) {
1494
+ throw new ModelNotRegisteredError("Subscription");
1495
+ }
1496
+ const SubscriptionModel = this.models.Subscription;
1497
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1498
+ if (!subscription) {
1499
+ throw new SubscriptionNotFoundError(subscriptionId);
1500
+ }
1501
+ const now = /* @__PURE__ */ new Date();
1502
+ if (immediate) {
1503
+ subscription.isActive = false;
1504
+ subscription.status = "cancelled";
1505
+ subscription.canceledAt = now;
1506
+ subscription.cancellationReason = reason;
1507
+ } else {
1508
+ subscription.cancelAt = subscription.endDate ?? now;
1509
+ subscription.cancellationReason = reason;
1510
+ }
1511
+ await subscription.save();
1512
+ this._triggerHook("subscription.cancelled", {
1513
+ subscription,
1514
+ immediate,
1515
+ reason,
1516
+ canceledAt: immediate ? now : subscription.cancelAt
1517
+ });
1518
+ return subscription;
1519
+ }
1520
+ /**
1521
+ * Pause subscription
1522
+ *
1523
+ * @param subscriptionId - Subscription ID
1524
+ * @param options - Pause options
1525
+ * @returns Updated subscription
1526
+ */
1527
+ async pause(subscriptionId, options = {}) {
1528
+ const { reason = null } = options;
1529
+ if (!this.models.Subscription) {
1530
+ throw new ModelNotRegisteredError("Subscription");
1531
+ }
1532
+ const SubscriptionModel = this.models.Subscription;
1533
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1534
+ if (!subscription) {
1535
+ throw new SubscriptionNotFoundError(subscriptionId);
1536
+ }
1537
+ if (!subscription.isActive) {
1538
+ throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
1539
+ }
1540
+ const pausedAt = /* @__PURE__ */ new Date();
1541
+ subscription.isActive = false;
1542
+ subscription.status = "paused";
1543
+ subscription.pausedAt = pausedAt;
1544
+ subscription.pauseReason = reason;
1545
+ await subscription.save();
1546
+ this._triggerHook("subscription.paused", {
1547
+ subscription,
1548
+ reason,
1549
+ pausedAt
1550
+ });
1551
+ return subscription;
1552
+ }
1553
+ /**
1554
+ * Resume subscription
1555
+ *
1556
+ * @param subscriptionId - Subscription ID
1557
+ * @param options - Resume options
1558
+ * @returns Updated subscription
1559
+ */
1560
+ async resume(subscriptionId, options = {}) {
1561
+ const { extendPeriod = false } = options;
1562
+ if (!this.models.Subscription) {
1563
+ throw new ModelNotRegisteredError("Subscription");
1564
+ }
1565
+ const SubscriptionModel = this.models.Subscription;
1566
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1567
+ if (!subscription) {
1568
+ throw new SubscriptionNotFoundError(subscriptionId);
1569
+ }
1570
+ if (!subscription.pausedAt) {
1571
+ throw new InvalidStateTransitionError(
1572
+ "resume",
1573
+ "paused",
1574
+ subscription.status,
1575
+ "Only paused subscriptions can be resumed"
1576
+ );
1577
+ }
1578
+ const now = /* @__PURE__ */ new Date();
1579
+ const pausedAt = new Date(subscription.pausedAt);
1580
+ const pauseDuration = now.getTime() - pausedAt.getTime();
1581
+ subscription.isActive = true;
1582
+ subscription.status = "active";
1583
+ subscription.pausedAt = null;
1584
+ subscription.pauseReason = null;
1585
+ if (extendPeriod && subscription.endDate) {
1586
+ const currentEnd = new Date(subscription.endDate);
1587
+ subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1588
+ }
1589
+ await subscription.save();
1590
+ this._triggerHook("subscription.resumed", {
1591
+ subscription,
1592
+ extendPeriod,
1593
+ pauseDuration,
1594
+ resumedAt: now
1595
+ });
1596
+ return subscription;
1597
+ }
1598
+ /**
1599
+ * List subscriptions with filters
1600
+ *
1601
+ * @param filters - Query filters
1602
+ * @param options - Query options (limit, skip, sort)
1603
+ * @returns Subscriptions
1604
+ */
1605
+ async list(filters = {}, options = {}) {
1606
+ if (!this.models.Subscription) {
1607
+ throw new ModelNotRegisteredError("Subscription");
1608
+ }
1609
+ const SubscriptionModel = this.models.Subscription;
1610
+ const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1611
+ const subscriptions = await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
1612
+ return subscriptions;
1613
+ }
1614
+ /**
1615
+ * Get subscription by ID
1616
+ *
1617
+ * @param subscriptionId - Subscription ID
1618
+ * @returns Subscription
1619
+ */
1620
+ async get(subscriptionId) {
1621
+ if (!this.models.Subscription) {
1622
+ throw new ModelNotRegisteredError("Subscription");
1623
+ }
1624
+ const SubscriptionModel = this.models.Subscription;
1625
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1626
+ if (!subscription) {
1627
+ throw new SubscriptionNotFoundError(subscriptionId);
1628
+ }
1629
+ return subscription;
1630
+ }
1631
+ /**
1632
+ * Calculate period end date based on plan key
1633
+ * @private
1634
+ */
1635
+ _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
1636
+ const start = new Date(startDate);
1637
+ const end = new Date(start);
1638
+ switch (planKey) {
1639
+ case "monthly":
1640
+ end.setMonth(end.getMonth() + 1);
1641
+ break;
1642
+ case "quarterly":
1643
+ end.setMonth(end.getMonth() + 3);
1644
+ break;
1645
+ case "yearly":
1646
+ end.setFullYear(end.getFullYear() + 1);
1647
+ break;
1648
+ default:
1649
+ end.setDate(end.getDate() + 30);
1650
+ }
1651
+ return end;
1652
+ }
1653
+ /**
1654
+ * Trigger event hook (fire-and-forget, non-blocking)
1655
+ * @private
1656
+ */
1657
+ _triggerHook(event, data) {
1658
+ triggerHook(this.hooks, event, data, this.logger);
1659
+ }
1660
+ };
1661
+
1662
+ // src/services/payment.service.ts
1663
+ var PaymentService = class {
1664
+ models;
1665
+ providers;
1666
+ config;
1667
+ hooks;
1668
+ logger;
1669
+ constructor(container) {
1670
+ this.models = container.get("models");
1671
+ this.providers = container.get("providers");
1672
+ this.config = container.get("config");
1673
+ this.hooks = container.get("hooks");
1674
+ this.logger = container.get("logger");
1675
+ }
1676
+ /**
1677
+ * Verify a payment
1678
+ *
1679
+ * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1680
+ * @param options - Verification options
1681
+ * @returns { transaction, status }
1682
+ */
1683
+ async verify(paymentIntentId, options = {}) {
1684
+ const { verifiedBy = null } = options;
1685
+ const TransactionModel = this.models.Transaction;
1686
+ const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1687
+ if (!transaction) {
1688
+ throw new TransactionNotFoundError(paymentIntentId);
1689
+ }
1690
+ if (transaction.status === "verified" || transaction.status === "completed") {
1691
+ throw new AlreadyVerifiedError(transaction._id.toString());
1692
+ }
1693
+ const gatewayType = transaction.gateway?.type ?? "manual";
1694
+ const provider = this.providers[gatewayType];
1695
+ if (!provider) {
1696
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1697
+ }
1698
+ let paymentResult = null;
1699
+ try {
1700
+ paymentResult = await provider.verifyPayment(paymentIntentId);
1701
+ } catch (error) {
1702
+ this.logger.error("Payment verification failed:", error);
1703
+ transaction.status = "failed";
1704
+ transaction.failureReason = error.message;
1705
+ transaction.metadata = {
1706
+ ...transaction.metadata,
1707
+ verificationError: error.message,
1708
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
1709
+ };
1710
+ await transaction.save();
1711
+ this._triggerHook("payment.failed", {
1712
+ transaction,
1713
+ error: error.message,
1714
+ provider: gatewayType,
1715
+ paymentIntentId
1716
+ });
1717
+ throw new PaymentVerificationError(paymentIntentId, error.message);
1718
+ }
1719
+ if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
1720
+ throw new ValidationError(
1721
+ `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
1722
+ { expected: transaction.amount, actual: paymentResult.amount }
1723
+ );
1724
+ }
1725
+ if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
1726
+ throw new ValidationError(
1727
+ `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
1728
+ { expected: transaction.currency, actual: paymentResult.currency }
1729
+ );
1730
+ }
1731
+ transaction.status = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1732
+ transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1733
+ transaction.verifiedBy = verifiedBy;
1734
+ transaction.gateway = {
1735
+ ...transaction.gateway,
1736
+ type: transaction.gateway?.type ?? "manual",
1737
+ verificationData: paymentResult.metadata
1738
+ };
1739
+ await transaction.save();
1740
+ this._triggerHook("payment.verified", {
1741
+ transaction,
1742
+ paymentResult,
1743
+ verifiedBy
1744
+ });
1745
+ return {
1746
+ transaction,
1747
+ paymentResult,
1748
+ status: transaction.status
1749
+ };
1750
+ }
1751
+ /**
1752
+ * Get payment status
1753
+ *
1754
+ * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1755
+ * @returns { transaction, status }
1756
+ */
1757
+ async getStatus(paymentIntentId) {
1758
+ const TransactionModel = this.models.Transaction;
1759
+ const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1760
+ if (!transaction) {
1761
+ throw new TransactionNotFoundError(paymentIntentId);
1762
+ }
1763
+ const gatewayType = transaction.gateway?.type ?? "manual";
1764
+ const provider = this.providers[gatewayType];
1765
+ if (!provider) {
1766
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1767
+ }
1768
+ let paymentResult = null;
1769
+ try {
1770
+ paymentResult = await provider.getStatus(paymentIntentId);
1771
+ } catch (error) {
1772
+ this.logger.warn("Failed to get payment status from provider:", error);
1773
+ return {
1774
+ transaction,
1775
+ status: transaction.status,
1776
+ provider: gatewayType
1777
+ };
1778
+ }
1779
+ return {
1780
+ transaction,
1781
+ paymentResult,
1782
+ status: paymentResult.status,
1783
+ provider: gatewayType
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Refund a payment
1788
+ *
1789
+ * @param paymentId - Payment intent ID, session ID, or transaction ID
1790
+ * @param amount - Amount to refund (optional, full refund if not provided)
1791
+ * @param options - Refund options
1792
+ * @returns { transaction, refundResult }
1793
+ */
1794
+ async refund(paymentId, amount = null, options = {}) {
1795
+ const { reason = null } = options;
1796
+ const TransactionModel = this.models.Transaction;
1797
+ const transaction = await this._findTransaction(TransactionModel, paymentId);
1798
+ if (!transaction) {
1799
+ throw new TransactionNotFoundError(paymentId);
1800
+ }
1801
+ if (transaction.status !== "verified" && transaction.status !== "completed") {
1802
+ throw new RefundError(transaction._id.toString(), "Only verified/completed transactions can be refunded");
1803
+ }
1804
+ const gatewayType = transaction.gateway?.type ?? "manual";
1805
+ const provider = this.providers[gatewayType];
1806
+ if (!provider) {
1807
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1808
+ }
1809
+ const capabilities = provider.getCapabilities();
1810
+ if (!capabilities.supportsRefunds) {
1811
+ throw new RefundNotSupportedError(gatewayType);
1812
+ }
1813
+ const refundedSoFar = transaction.refundedAmount ?? 0;
1814
+ const refundableAmount = transaction.amount - refundedSoFar;
1815
+ const refundAmount = amount ?? refundableAmount;
1816
+ if (refundAmount <= 0) {
1817
+ throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1818
+ }
1819
+ if (refundAmount > refundableAmount) {
1820
+ throw new ValidationError(
1821
+ `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
1822
+ { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
1823
+ );
1824
+ }
1825
+ let refundResult;
1826
+ try {
1827
+ refundResult = await provider.refund(paymentId, refundAmount, { reason: reason ?? void 0 });
1828
+ } catch (error) {
1829
+ this.logger.error("Refund failed:", error);
1830
+ throw new RefundError(paymentId, error.message);
1831
+ }
1832
+ const refundTransactionType = this.config.transactionTypeMapping?.refund ?? TRANSACTION_TYPE.EXPENSE;
1833
+ const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1834
+ const refundTransaction = await TransactionModel.create({
1835
+ organizationId: transaction.organizationId,
1836
+ customerId: transaction.customerId,
1837
+ amount: refundAmount,
1838
+ currency: transaction.currency,
1839
+ category: transaction.category,
1840
+ type: refundTransactionType,
1841
+ // EXPENSE - money going out
1842
+ method: transaction.method ?? "manual",
1843
+ status: "completed",
1844
+ gateway: {
1845
+ type: transaction.gateway?.type ?? "manual",
1846
+ paymentIntentId: refundResult.id,
1847
+ provider: refundResult.provider
1848
+ },
1849
+ paymentDetails: transaction.paymentDetails,
1850
+ ...refundCommission && { commission: refundCommission },
1851
+ // Reversed commission
1852
+ // Polymorphic reference (copy from original transaction)
1853
+ ...transaction.referenceId && { referenceId: transaction.referenceId },
1854
+ ...transaction.referenceModel && { referenceModel: transaction.referenceModel },
1855
+ metadata: {
1856
+ ...transaction.metadata,
1857
+ isRefund: true,
1858
+ originalTransactionId: transaction._id.toString(),
1859
+ refundReason: reason,
1860
+ refundResult: refundResult.metadata
1861
+ },
1862
+ idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1863
+ });
1864
+ const isPartialRefund = refundAmount < transaction.amount;
1865
+ transaction.status = isPartialRefund ? "partially_refunded" : "refunded";
1866
+ transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1867
+ transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1868
+ transaction.metadata = {
1869
+ ...transaction.metadata,
1870
+ refundTransactionId: refundTransaction._id.toString(),
1871
+ refundReason: reason
1872
+ };
1873
+ await transaction.save();
1874
+ this._triggerHook("payment.refunded", {
1875
+ transaction,
1876
+ refundTransaction,
1877
+ refundResult,
1878
+ refundAmount,
1879
+ reason,
1880
+ isPartialRefund
1881
+ });
1882
+ return {
1883
+ transaction,
1884
+ refundTransaction,
1885
+ refundResult,
1886
+ status: transaction.status
1887
+ };
1888
+ }
1889
+ /**
1890
+ * Handle webhook from payment provider
1891
+ *
1892
+ * @param provider - Provider name
1893
+ * @param payload - Webhook payload
1894
+ * @param headers - Request headers
1895
+ * @returns { event, transaction }
1896
+ */
1897
+ async handleWebhook(providerName, payload, headers = {}) {
1898
+ const provider = this.providers[providerName];
1899
+ if (!provider) {
1900
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1901
+ }
1902
+ const capabilities = provider.getCapabilities();
1903
+ if (!capabilities.supportsWebhooks) {
1904
+ throw new ProviderCapabilityError(providerName, "webhooks");
1905
+ }
1906
+ let webhookEvent;
1907
+ try {
1908
+ webhookEvent = await provider.handleWebhook(payload, headers);
1909
+ } catch (error) {
1910
+ this.logger.error("Webhook processing failed:", error);
1911
+ throw new ProviderError(
1912
+ `Webhook processing failed for ${providerName}: ${error.message}`,
1913
+ "WEBHOOK_PROCESSING_FAILED",
1914
+ { retryable: false }
1915
+ );
1916
+ }
1917
+ if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) {
1918
+ throw new ValidationError(
1919
+ `Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`,
1920
+ { provider: providerName, eventType: webhookEvent?.type }
1921
+ );
1922
+ }
1923
+ const TransactionModel = this.models.Transaction;
1924
+ let transaction = null;
1925
+ if (webhookEvent.data.sessionId) {
1926
+ transaction = await TransactionModel.findOne({
1927
+ "gateway.sessionId": webhookEvent.data.sessionId
1928
+ });
1929
+ }
1930
+ if (!transaction && webhookEvent.data.paymentIntentId) {
1931
+ transaction = await TransactionModel.findOne({
1932
+ "gateway.paymentIntentId": webhookEvent.data.paymentIntentId
1933
+ });
1934
+ }
1935
+ if (!transaction) {
1936
+ this.logger.warn("Transaction not found for webhook event", {
1937
+ provider: providerName,
1938
+ eventId: webhookEvent.id,
1939
+ sessionId: webhookEvent.data.sessionId,
1940
+ paymentIntentId: webhookEvent.data.paymentIntentId
1941
+ });
1942
+ throw new TransactionNotFoundError(
1943
+ webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown"
1944
+ );
1945
+ }
1946
+ if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) {
1947
+ transaction.gateway = {
1948
+ ...transaction.gateway,
1949
+ type: transaction.gateway?.type ?? "manual",
1950
+ sessionId: webhookEvent.data.sessionId
1951
+ };
1952
+ }
1953
+ if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) {
1954
+ transaction.gateway = {
1955
+ ...transaction.gateway,
1956
+ type: transaction.gateway?.type ?? "manual",
1957
+ paymentIntentId: webhookEvent.data.paymentIntentId
1958
+ };
1959
+ }
1960
+ if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
1961
+ this.logger.warn("Webhook already processed", {
1962
+ transactionId: transaction._id,
1963
+ eventId: webhookEvent.id
1964
+ });
1965
+ return {
1966
+ event: webhookEvent,
1967
+ transaction,
1968
+ status: "already_processed"
1969
+ };
1970
+ }
1971
+ transaction.webhook = {
1972
+ eventId: webhookEvent.id,
1973
+ eventType: webhookEvent.type,
1974
+ receivedAt: /* @__PURE__ */ new Date(),
1975
+ processedAt: /* @__PURE__ */ new Date(),
1976
+ data: webhookEvent.data
1977
+ };
1978
+ if (webhookEvent.type === "payment.succeeded") {
1979
+ transaction.status = "verified";
1980
+ transaction.verifiedAt = webhookEvent.createdAt;
1981
+ } else if (webhookEvent.type === "payment.failed") {
1982
+ transaction.status = "failed";
1983
+ } else if (webhookEvent.type === "refund.succeeded") {
1984
+ transaction.status = "refunded";
1985
+ transaction.refundedAt = webhookEvent.createdAt;
1986
+ }
1987
+ await transaction.save();
1988
+ this._triggerHook(`payment.webhook.${webhookEvent.type}`, {
1989
+ event: webhookEvent,
1990
+ transaction
1991
+ });
1992
+ return {
1993
+ event: webhookEvent,
1994
+ transaction,
1995
+ status: "processed"
1996
+ };
1997
+ }
1998
+ /**
1999
+ * List payments/transactions with filters
2000
+ *
2001
+ * @param filters - Query filters
2002
+ * @param options - Query options (limit, skip, sort)
2003
+ * @returns Transactions
2004
+ */
2005
+ async list(filters = {}, options = {}) {
2006
+ const TransactionModel = this.models.Transaction;
2007
+ const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
2008
+ const transactions = await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
2009
+ return transactions;
2010
+ }
2011
+ /**
2012
+ * Get payment/transaction by ID
2013
+ *
2014
+ * @param transactionId - Transaction ID
2015
+ * @returns Transaction
2016
+ */
2017
+ async get(transactionId) {
2018
+ const TransactionModel = this.models.Transaction;
2019
+ const transaction = await TransactionModel.findById(transactionId);
2020
+ if (!transaction) {
2021
+ throw new TransactionNotFoundError(transactionId);
2022
+ }
2023
+ return transaction;
2024
+ }
2025
+ /**
2026
+ * Get provider instance
2027
+ *
2028
+ * @param providerName - Provider name
2029
+ * @returns Provider instance
2030
+ */
2031
+ getProvider(providerName) {
2032
+ const provider = this.providers[providerName];
2033
+ if (!provider) {
2034
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
2035
+ }
2036
+ return provider;
2037
+ }
2038
+ /**
2039
+ * Trigger event hook (fire-and-forget, non-blocking)
2040
+ * @private
2041
+ */
2042
+ _triggerHook(event, data) {
2043
+ triggerHook(this.hooks, event, data, this.logger);
2044
+ }
2045
+ /**
2046
+ * Find transaction by sessionId, paymentIntentId, or transaction ID
2047
+ * @private
2048
+ */
2049
+ async _findTransaction(TransactionModel, identifier) {
2050
+ let transaction = await TransactionModel.findOne({
2051
+ "gateway.sessionId": identifier
2052
+ });
2053
+ if (!transaction) {
2054
+ transaction = await TransactionModel.findOne({
2055
+ "gateway.paymentIntentId": identifier
2056
+ });
2057
+ }
2058
+ if (!transaction) {
2059
+ transaction = await TransactionModel.findById(identifier);
2060
+ }
2061
+ return transaction;
2062
+ }
2063
+ };
2064
+
2065
+ // src/services/transaction.service.ts
2066
+ var TransactionService = class {
2067
+ models;
2068
+ hooks;
2069
+ logger;
2070
+ constructor(container) {
2071
+ this.models = container.get("models");
2072
+ this.hooks = container.get("hooks");
2073
+ this.logger = container.get("logger");
2074
+ }
2075
+ /**
2076
+ * Get transaction by ID
2077
+ *
2078
+ * @param transactionId - Transaction ID
2079
+ * @returns Transaction
2080
+ */
2081
+ async get(transactionId) {
2082
+ const TransactionModel = this.models.Transaction;
2083
+ const transaction = await TransactionModel.findById(transactionId);
2084
+ if (!transaction) {
2085
+ throw new TransactionNotFoundError(transactionId);
2086
+ }
2087
+ return transaction;
2088
+ }
2089
+ /**
2090
+ * List transactions with filters
2091
+ *
2092
+ * @param filters - Query filters
2093
+ * @param options - Query options (limit, skip, sort, populate)
2094
+ * @returns { transactions, total, page, limit }
2095
+ */
2096
+ async list(filters = {}, options = {}) {
2097
+ const TransactionModel = this.models.Transaction;
2098
+ const {
2099
+ limit = 50,
2100
+ skip = 0,
2101
+ page = null,
2102
+ sort = { createdAt: -1 },
2103
+ populate = []
2104
+ } = options;
2105
+ const actualSkip = page ? (page - 1) * limit : skip;
2106
+ let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
2107
+ if (populate.length > 0 && typeof query.populate === "function") {
2108
+ populate.forEach((field) => {
2109
+ query = query.populate(field);
2110
+ });
2111
+ }
2112
+ const transactions = await query;
2113
+ const model = TransactionModel;
2114
+ const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
2115
+ return {
2116
+ transactions,
2117
+ total,
2118
+ page: page ?? Math.floor(actualSkip / limit) + 1,
2119
+ limit,
2120
+ pages: Math.ceil(total / limit)
2121
+ };
2122
+ }
2123
+ /**
2124
+ * Update transaction
2125
+ *
2126
+ * @param transactionId - Transaction ID
2127
+ * @param updates - Fields to update
2128
+ * @returns Updated transaction
2129
+ */
2130
+ async update(transactionId, updates) {
2131
+ const TransactionModel = this.models.Transaction;
2132
+ const model = TransactionModel;
2133
+ let transaction;
2134
+ if (typeof model.update === "function") {
2135
+ transaction = await model.update(transactionId, updates);
2136
+ } else if (typeof model.findByIdAndUpdate === "function") {
2137
+ transaction = await model.findByIdAndUpdate(
2138
+ transactionId,
2139
+ { $set: updates },
2140
+ { new: true }
2141
+ );
2142
+ } else {
2143
+ throw new Error("Transaction model does not support update operations");
2144
+ }
2145
+ if (!transaction) {
2146
+ throw new TransactionNotFoundError(transactionId);
2147
+ }
2148
+ this._triggerHook("transaction.updated", {
2149
+ transaction,
2150
+ updates
2151
+ });
2152
+ return transaction;
2153
+ }
2154
+ /**
2155
+ * Trigger event hook (fire-and-forget, non-blocking)
2156
+ * @private
2157
+ */
2158
+ _triggerHook(event, data) {
2159
+ triggerHook(this.hooks, event, data, this.logger);
2160
+ }
2161
+ };
2162
+
2163
+ // src/enums/escrow.enums.ts
2164
+ var HOLD_STATUS = {
2165
+ HELD: "held",
2166
+ RELEASED: "released",
2167
+ CANCELLED: "cancelled",
2168
+ PARTIALLY_RELEASED: "partially_released"
2169
+ };
2170
+ var RELEASE_REASON = {
2171
+ PAYMENT_VERIFIED: "payment_verified"};
2172
+ var HOLD_REASON = {
2173
+ PAYMENT_VERIFICATION: "payment_verification"};
2174
+
2175
+ // src/enums/split.enums.ts
2176
+ var SPLIT_TYPE = {
2177
+ CUSTOM: "custom"
2178
+ };
2179
+ var SPLIT_STATUS = {
2180
+ PENDING: "pending",
2181
+ PAID: "paid"};
2182
+
2183
+ // src/utils/commission-split.ts
2184
+ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2185
+ if (!splitRules || splitRules.length === 0) {
2186
+ return [];
2187
+ }
2188
+ if (amount < 0) {
2189
+ throw new Error("Transaction amount cannot be negative");
2190
+ }
2191
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
2192
+ throw new Error("Gateway fee rate must be between 0 and 1");
2193
+ }
2194
+ const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
2195
+ if (totalRate > 1) {
2196
+ throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
2197
+ }
2198
+ return splitRules.map((rule, index) => {
2199
+ if (rule.rate < 0 || rule.rate > 1) {
2200
+ throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
2201
+ }
2202
+ const grossAmount = Math.round(amount * rule.rate * 100) / 100;
2203
+ const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
2204
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
2205
+ return {
2206
+ type: rule.type ?? SPLIT_TYPE.CUSTOM,
2207
+ recipientId: rule.recipientId,
2208
+ recipientType: rule.recipientType,
2209
+ rate: rule.rate,
2210
+ grossAmount,
2211
+ gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
2212
+ gatewayFeeAmount,
2213
+ netAmount,
2214
+ status: SPLIT_STATUS.PENDING,
2215
+ dueDate: rule.dueDate ?? null,
2216
+ metadata: rule.metadata ?? {}
2217
+ };
2218
+ });
2219
+ }
2220
+ function calculateOrganizationPayout(amount, splits = []) {
2221
+ const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
2222
+ return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
2223
+ }
2224
+
2225
+ // src/services/escrow.service.ts
2226
+ var EscrowService = class {
2227
+ models;
2228
+ hooks;
2229
+ logger;
2230
+ constructor(container) {
2231
+ this.models = container.get("models");
2232
+ this.hooks = container.get("hooks");
2233
+ this.logger = container.get("logger");
2234
+ }
2235
+ /**
2236
+ * Hold funds in escrow
2237
+ *
2238
+ * @param transactionId - Transaction to hold
2239
+ * @param options - Hold options
2240
+ * @returns Updated transaction
2241
+ */
2242
+ async hold(transactionId, options = {}) {
2243
+ const {
2244
+ reason = HOLD_REASON.PAYMENT_VERIFICATION,
2245
+ holdUntil = null,
2246
+ metadata = {}
2247
+ } = options;
2248
+ const TransactionModel = this.models.Transaction;
2249
+ const transaction = await TransactionModel.findById(transactionId);
2250
+ if (!transaction) {
2251
+ throw new TransactionNotFoundError(transactionId);
2252
+ }
2253
+ if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
2254
+ throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
2255
+ }
2256
+ transaction.hold = {
2257
+ status: HOLD_STATUS.HELD,
2258
+ heldAmount: transaction.amount,
2259
+ releasedAmount: 0,
2260
+ reason,
2261
+ heldAt: /* @__PURE__ */ new Date(),
2262
+ ...holdUntil && { holdUntil },
2263
+ releases: [],
2264
+ metadata
2265
+ };
2266
+ await transaction.save();
2267
+ this._triggerHook("escrow.held", {
2268
+ transaction,
2269
+ heldAmount: transaction.amount,
2270
+ reason
2271
+ });
2272
+ return transaction;
2273
+ }
2274
+ /**
2275
+ * Release funds from escrow to recipient
2276
+ *
2277
+ * @param transactionId - Transaction to release
2278
+ * @param options - Release options
2279
+ * @returns { transaction, releaseTransaction }
2280
+ */
2281
+ async release(transactionId, options) {
2282
+ const {
2283
+ amount = null,
2284
+ recipientId,
2285
+ recipientType = "organization",
2286
+ reason = RELEASE_REASON.PAYMENT_VERIFIED,
2287
+ releasedBy = null,
2288
+ createTransaction = true,
2289
+ metadata = {}
2290
+ } = options;
2291
+ const TransactionModel = this.models.Transaction;
2292
+ const transaction = await TransactionModel.findById(transactionId);
2293
+ if (!transaction) {
2294
+ throw new TransactionNotFoundError(transactionId);
2295
+ }
2296
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2297
+ throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2298
+ }
2299
+ if (!recipientId) {
2300
+ throw new Error("recipientId is required for release");
2301
+ }
2302
+ const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
2303
+ const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
2304
+ if (releaseAmount > availableAmount) {
2305
+ throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
2306
+ }
2307
+ const releaseRecord = {
2308
+ amount: releaseAmount,
2309
+ recipientId,
2310
+ recipientType,
2311
+ releasedAt: /* @__PURE__ */ new Date(),
2312
+ releasedBy,
2313
+ reason,
2314
+ metadata
2315
+ };
2316
+ transaction.hold.releases.push(releaseRecord);
2317
+ transaction.hold.releasedAmount += releaseAmount;
2318
+ const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
2319
+ const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
2320
+ if (isFullRelease) {
2321
+ transaction.hold.status = HOLD_STATUS.RELEASED;
2322
+ transaction.hold.releasedAt = /* @__PURE__ */ new Date();
2323
+ transaction.status = TRANSACTION_STATUS.COMPLETED;
2324
+ } else if (isPartialRelease) {
2325
+ transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
2326
+ }
2327
+ await transaction.save();
2328
+ let releaseTransaction = null;
2329
+ if (createTransaction) {
2330
+ releaseTransaction = await TransactionModel.create({
2331
+ organizationId: transaction.organizationId,
2332
+ customerId: recipientId,
2333
+ amount: releaseAmount,
2334
+ currency: transaction.currency,
2335
+ category: transaction.category,
2336
+ type: TRANSACTION_TYPE.INCOME,
2337
+ method: transaction.method,
2338
+ status: TRANSACTION_STATUS.COMPLETED,
2339
+ gateway: transaction.gateway,
2340
+ referenceId: transaction.referenceId,
2341
+ referenceModel: transaction.referenceModel,
2342
+ metadata: {
2343
+ ...metadata,
2344
+ isRelease: true,
2345
+ heldTransactionId: transaction._id.toString(),
2346
+ releaseReason: reason,
2347
+ recipientType
2348
+ },
2349
+ idempotencyKey: `release_${transaction._id}_${Date.now()}`
2350
+ });
2351
+ }
2352
+ this._triggerHook("escrow.released", {
2353
+ transaction,
2354
+ releaseTransaction,
2355
+ releaseAmount,
2356
+ recipientId,
2357
+ recipientType,
2358
+ reason,
2359
+ isFullRelease,
2360
+ isPartialRelease
2361
+ });
2362
+ return {
2363
+ transaction,
2364
+ releaseTransaction,
2365
+ releaseAmount,
2366
+ isFullRelease,
2367
+ isPartialRelease
2368
+ };
2369
+ }
2370
+ /**
2371
+ * Cancel hold and release back to customer
2372
+ *
2373
+ * @param transactionId - Transaction to cancel hold
2374
+ * @param options - Cancel options
2375
+ * @returns Updated transaction
2376
+ */
2377
+ async cancel(transactionId, options = {}) {
2378
+ const { reason = "Hold cancelled", metadata = {} } = options;
2379
+ const TransactionModel = this.models.Transaction;
2380
+ const transaction = await TransactionModel.findById(transactionId);
2381
+ if (!transaction) {
2382
+ throw new TransactionNotFoundError(transactionId);
2383
+ }
2384
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2385
+ throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2386
+ }
2387
+ transaction.hold.status = HOLD_STATUS.CANCELLED;
2388
+ transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
2389
+ transaction.hold.metadata = {
2390
+ ...transaction.hold.metadata,
2391
+ ...metadata,
2392
+ cancelReason: reason
2393
+ };
2394
+ transaction.status = TRANSACTION_STATUS.CANCELLED;
2395
+ await transaction.save();
2396
+ this._triggerHook("escrow.cancelled", {
2397
+ transaction,
2398
+ reason
2399
+ });
2400
+ return transaction;
2401
+ }
2402
+ /**
2403
+ * Split payment to multiple recipients
2404
+ * Deducts splits from held amount and releases remainder to organization
2405
+ *
2406
+ * @param transactionId - Transaction to split
2407
+ * @param splitRules - Split configuration
2408
+ * @returns { transaction, splitTransactions, organizationTransaction }
2409
+ */
2410
+ async split(transactionId, splitRules = []) {
2411
+ const TransactionModel = this.models.Transaction;
2412
+ const transaction = await TransactionModel.findById(transactionId);
2413
+ if (!transaction) {
2414
+ throw new TransactionNotFoundError(transactionId);
2415
+ }
2416
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2417
+ throw new Error(`Transaction must be held before splitting. Current: ${transaction.hold?.status ?? "none"}`);
2418
+ }
2419
+ if (!splitRules || splitRules.length === 0) {
2420
+ throw new Error("splitRules cannot be empty");
2421
+ }
2422
+ const splits = calculateSplits(
2423
+ transaction.amount,
2424
+ splitRules,
2425
+ transaction.commission?.gatewayFeeRate ?? 0
2426
+ );
2427
+ transaction.splits = splits;
2428
+ await transaction.save();
2429
+ const splitTransactions = [];
2430
+ for (const split of splits) {
2431
+ const splitTransaction = await TransactionModel.create({
2432
+ organizationId: transaction.organizationId,
2433
+ customerId: split.recipientId,
2434
+ amount: split.netAmount,
2435
+ currency: transaction.currency,
2436
+ category: split.type,
2437
+ type: TRANSACTION_TYPE.EXPENSE,
2438
+ method: transaction.method,
2439
+ status: TRANSACTION_STATUS.COMPLETED,
2440
+ gateway: transaction.gateway,
2441
+ referenceId: transaction.referenceId,
2442
+ referenceModel: transaction.referenceModel,
2443
+ metadata: {
2444
+ isSplit: true,
2445
+ splitType: split.type,
2446
+ recipientType: split.recipientType,
2447
+ originalTransactionId: transaction._id.toString(),
2448
+ grossAmount: split.grossAmount,
2449
+ gatewayFeeAmount: split.gatewayFeeAmount
2450
+ },
2451
+ idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
2452
+ });
2453
+ split.payoutTransactionId = splitTransaction._id.toString();
2454
+ split.status = SPLIT_STATUS.PAID;
2455
+ split.paidDate = /* @__PURE__ */ new Date();
2456
+ splitTransactions.push(splitTransaction);
2457
+ }
2458
+ await transaction.save();
2459
+ const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
2460
+ const organizationTransaction = await this.release(transactionId, {
2461
+ amount: organizationPayout,
2462
+ recipientId: transaction.organizationId?.toString() ?? "",
2463
+ recipientType: "organization",
2464
+ reason: RELEASE_REASON.PAYMENT_VERIFIED,
2465
+ createTransaction: true,
2466
+ metadata: {
2467
+ afterSplits: true,
2468
+ totalSplits: splits.length,
2469
+ totalSplitAmount: transaction.amount - organizationPayout
2470
+ }
2471
+ });
2472
+ this._triggerHook("escrow.split", {
2473
+ transaction,
2474
+ splits,
2475
+ splitTransactions,
2476
+ organizationTransaction: organizationTransaction.releaseTransaction,
2477
+ organizationPayout
2478
+ });
2479
+ return {
2480
+ transaction,
2481
+ splits,
2482
+ splitTransactions,
2483
+ organizationTransaction: organizationTransaction.releaseTransaction,
2484
+ organizationPayout
2485
+ };
2486
+ }
2487
+ /**
2488
+ * Get escrow status
2489
+ *
2490
+ * @param transactionId - Transaction ID
2491
+ * @returns Escrow status
2492
+ */
2493
+ async getStatus(transactionId) {
2494
+ const TransactionModel = this.models.Transaction;
2495
+ const transaction = await TransactionModel.findById(transactionId);
2496
+ if (!transaction) {
2497
+ throw new TransactionNotFoundError(transactionId);
2498
+ }
2499
+ return {
2500
+ transaction,
2501
+ hold: transaction.hold ?? null,
2502
+ splits: transaction.splits ?? [],
2503
+ hasHold: !!transaction.hold,
2504
+ hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2505
+ };
2506
+ }
2507
+ _triggerHook(event, data) {
2508
+ triggerHook(this.hooks, event, data, this.logger);
2509
+ }
2510
+ };
2511
+
2512
+ // src/core/revenue.ts
2513
+ var Revenue = class {
2514
+ // ============ CORE ============
2515
+ _container;
2516
+ _events;
2517
+ _plugins;
2518
+ _idempotency;
2519
+ _circuitBreaker;
2520
+ _options;
2521
+ _logger;
2522
+ _providers;
2523
+ _config;
2524
+ // ============ SERVICES ============
2525
+ /** Monetization service - purchases, subscriptions, free items */
2526
+ monetization;
2527
+ /** Payment service - verify, refund, webhooks */
2528
+ payments;
2529
+ /** Transaction service - query, update transactions */
2530
+ transactions;
2531
+ /** Escrow service - hold, release, splits */
2532
+ escrow;
2533
+ constructor(container, events, plugins, options, providers, config) {
2534
+ this._container = container;
2535
+ this._events = events;
2536
+ this._plugins = plugins;
2537
+ this._options = options;
2538
+ this._logger = options.logger;
2539
+ this._providers = providers;
2540
+ this._config = config;
2541
+ this._idempotency = createIdempotencyManager({
2542
+ ttl: options.idempotencyTtl
2543
+ });
2544
+ if (options.circuitBreaker) {
2545
+ this._circuitBreaker = createCircuitBreaker();
2546
+ }
2547
+ container.singleton("events", events);
2548
+ container.singleton("plugins", plugins);
2549
+ container.singleton("idempotency", this._idempotency);
2550
+ container.singleton("logger", this._logger);
2551
+ this.monetization = new MonetizationService(container);
2552
+ this.payments = new PaymentService(container);
2553
+ this.transactions = new TransactionService(container);
2554
+ this.escrow = new EscrowService(container);
2555
+ Object.freeze(this._providers);
2556
+ Object.freeze(this._config);
2557
+ }
2558
+ // ============ STATIC FACTORY ============
2559
+ /**
2560
+ * Create a new Revenue builder
2561
+ *
2562
+ * @example
2563
+ * ```typescript
2564
+ * const revenue = Revenue
2565
+ * .create({ defaultCurrency: 'BDT' })
2566
+ * .withModels({ Transaction, Subscription })
2567
+ * .withProvider('manual', new ManualProvider())
2568
+ * .build();
2569
+ * ```
2570
+ */
2571
+ static create(options = {}) {
2572
+ return new RevenueBuilder(options);
2573
+ }
2574
+ // ============ ACCESSORS ============
2575
+ /** DI container (for advanced usage) */
2576
+ get container() {
2577
+ return this._container;
2578
+ }
2579
+ /** Event bus */
2580
+ get events() {
2581
+ return this._events;
2582
+ }
2583
+ /** Registered providers (frozen) */
2584
+ get providers() {
2585
+ return this._providers;
2586
+ }
2587
+ /** Configuration (frozen) */
2588
+ get config() {
2589
+ return this._config;
2590
+ }
2591
+ /** Default currency */
2592
+ get defaultCurrency() {
2593
+ return this._options.defaultCurrency;
2594
+ }
2595
+ /** Current environment */
2596
+ get environment() {
2597
+ return this._options.environment;
2598
+ }
2599
+ // ============ PROVIDER METHODS ============
2600
+ /**
2601
+ * Get a provider by name
2602
+ */
2603
+ getProvider(name) {
2604
+ const provider = this._providers[name];
2605
+ if (!provider) {
2606
+ throw new ConfigurationError(
2607
+ `Provider "${name}" not found. Available: ${Object.keys(this._providers).join(", ")}`
2608
+ );
2609
+ }
2610
+ return provider;
2611
+ }
2612
+ /**
2613
+ * Get all provider names
2614
+ */
2615
+ getProviderNames() {
2616
+ return Object.keys(this._providers);
2617
+ }
2618
+ /**
2619
+ * Check if provider exists
2620
+ */
2621
+ hasProvider(name) {
2622
+ return name in this._providers;
2623
+ }
2624
+ // ============ EVENT SYSTEM ============
2625
+ /**
2626
+ * Subscribe to events
2627
+ *
2628
+ * @example
2629
+ * ```typescript
2630
+ * revenue.on('payment.succeeded', (event) => {
2631
+ * console.log('Payment:', event.transactionId);
2632
+ * });
2633
+ * ```
2634
+ */
2635
+ on = (event, handler) => {
2636
+ return this._events.on(event, handler);
2637
+ };
2638
+ /**
2639
+ * Subscribe once
2640
+ */
2641
+ once = (event, handler) => {
2642
+ return this._events.once(event, handler);
2643
+ };
2644
+ /**
2645
+ * Unsubscribe
2646
+ */
2647
+ off = (event, handler) => {
2648
+ this._events.off(event, handler);
2649
+ };
2650
+ /**
2651
+ * Emit an event
2652
+ */
2653
+ emit = (event, payload) => {
2654
+ this._events.emit(event, payload);
2655
+ };
2656
+ // ============ RESILIENCE ============
2657
+ /**
2658
+ * Execute operation with retry and idempotency
2659
+ */
2660
+ async execute(operation, options = {}) {
2661
+ const { idempotencyKey, params, useRetry = true, useCircuitBreaker = true } = options;
2662
+ const idempotentOp = async () => {
2663
+ if (idempotencyKey) {
2664
+ const result = await this._idempotency.execute(idempotencyKey, params, operation);
2665
+ if (!result.ok) throw result.error;
2666
+ return result.value;
2667
+ }
2668
+ return operation();
2669
+ };
2670
+ const resilientOp = async () => {
2671
+ if (useCircuitBreaker && this._circuitBreaker) {
2672
+ return this._circuitBreaker.execute(idempotentOp);
2673
+ }
2674
+ return idempotentOp();
2675
+ };
2676
+ if (useRetry && this._options.retry) {
2677
+ return tryCatch(() => retry(resilientOp, this._options.retry));
2678
+ }
2679
+ return tryCatch(resilientOp);
2680
+ }
2681
+ /**
2682
+ * Create plugin context (for advanced usage)
2683
+ */
2684
+ createContext(meta = {}) {
2685
+ return {
2686
+ events: this._events,
2687
+ logger: this._logger,
2688
+ get: (key) => this._container.get(key),
2689
+ storage: /* @__PURE__ */ new Map(),
2690
+ meta: {
2691
+ ...meta,
2692
+ requestId: nanoid(12),
2693
+ timestamp: /* @__PURE__ */ new Date()
2694
+ }
2695
+ };
2696
+ }
2697
+ /**
2698
+ * Destroy instance and cleanup
2699
+ */
2700
+ async destroy() {
2701
+ await this._plugins.destroy();
2702
+ this._events.clear();
2703
+ }
2704
+ };
2705
+ var RevenueBuilder = class {
2706
+ options;
2707
+ models = null;
2708
+ providers = {};
2709
+ plugins = [];
2710
+ hooks = {};
2711
+ categoryMappings = {};
2712
+ constructor(options = {}) {
2713
+ this.options = options;
2714
+ }
2715
+ /**
2716
+ * Register models (required)
2717
+ *
2718
+ * @example
2719
+ * ```typescript
2720
+ * .withModels({
2721
+ * Transaction: TransactionModel,
2722
+ * Subscription: SubscriptionModel,
2723
+ * })
2724
+ * ```
2725
+ */
2726
+ withModels(models) {
2727
+ this.models = models;
2728
+ return this;
2729
+ }
2730
+ /**
2731
+ * Register a single model
2732
+ */
2733
+ withModel(name, model) {
2734
+ if (!this.models) {
2735
+ this.models = { Transaction: model };
2736
+ }
2737
+ this.models[name] = model;
2738
+ return this;
2739
+ }
2740
+ /**
2741
+ * Register a payment provider
2742
+ *
2743
+ * @example
2744
+ * ```typescript
2745
+ * .withProvider('stripe', new StripeProvider({ apiKey: '...' }))
2746
+ * .withProvider('manual', new ManualProvider())
2747
+ * ```
2748
+ */
2749
+ withProvider(name, provider) {
2750
+ this.providers[name] = provider;
2751
+ return this;
2752
+ }
2753
+ /**
2754
+ * Register multiple providers at once
2755
+ */
2756
+ withProviders(providers) {
2757
+ this.providers = { ...this.providers, ...providers };
2758
+ return this;
2759
+ }
2760
+ /**
2761
+ * Register a plugin
2762
+ *
2763
+ * @example
2764
+ * ```typescript
2765
+ * .withPlugin(loggingPlugin())
2766
+ * .withPlugin(auditPlugin({ store: saveToDb }))
2767
+ * ```
2768
+ */
2769
+ withPlugin(plugin) {
2770
+ this.plugins.push(plugin);
2771
+ return this;
2772
+ }
2773
+ /**
2774
+ * Register multiple plugins
2775
+ */
2776
+ withPlugins(plugins) {
2777
+ this.plugins.push(...plugins);
2778
+ return this;
2779
+ }
2780
+ /**
2781
+ * Register event hooks (for backward compatibility)
2782
+ *
2783
+ * @example
2784
+ * ```typescript
2785
+ * .withHooks({
2786
+ * onPaymentVerified: async (tx) => { ... },
2787
+ * onSubscriptionRenewed: async (sub) => { ... },
2788
+ * })
2789
+ * ```
2790
+ */
2791
+ withHooks(hooks) {
2792
+ this.hooks = { ...this.hooks, ...hooks };
2793
+ return this;
2794
+ }
2795
+ /**
2796
+ * Set retry configuration
2797
+ *
2798
+ * @example
2799
+ * ```typescript
2800
+ * .withRetry({ maxAttempts: 5, baseDelay: 2000 })
2801
+ * ```
2802
+ */
2803
+ withRetry(config) {
2804
+ this.options.retry = config;
2805
+ return this;
2806
+ }
2807
+ /**
2808
+ * Enable circuit breaker
2809
+ */
2810
+ withCircuitBreaker(enabled = true) {
2811
+ this.options.circuitBreaker = enabled;
2812
+ return this;
2813
+ }
2814
+ /**
2815
+ * Set custom logger
2816
+ */
2817
+ withLogger(logger) {
2818
+ this.options.logger = logger;
2819
+ return this;
2820
+ }
2821
+ /**
2822
+ * Set environment
2823
+ */
2824
+ forEnvironment(env) {
2825
+ this.options.environment = env;
2826
+ return this;
2827
+ }
2828
+ /**
2829
+ * Enable debug mode
2830
+ */
2831
+ withDebug(enabled = true) {
2832
+ this.options.debug = enabled;
2833
+ return this;
2834
+ }
2835
+ /**
2836
+ * Set commission rate (0-100)
2837
+ */
2838
+ withCommission(rate, gatewayFeeRate = 0) {
2839
+ this.options.commissionRate = rate;
2840
+ this.options.gatewayFeeRate = gatewayFeeRate;
2841
+ return this;
2842
+ }
2843
+ /**
2844
+ * Set category mappings (entity → category)
2845
+ *
2846
+ * @example
2847
+ * ```typescript
2848
+ * .withCategoryMappings({
2849
+ * PlatformSubscription: 'platform_subscription',
2850
+ * CourseEnrollment: 'course_enrollment',
2851
+ * ProductOrder: 'product_order',
2852
+ * })
2853
+ * ```
2854
+ */
2855
+ withCategoryMappings(mappings) {
2856
+ this.categoryMappings = { ...this.categoryMappings, ...mappings };
2857
+ return this;
2858
+ }
2859
+ /**
2860
+ * Build the Revenue instance
2861
+ */
2862
+ build() {
2863
+ if (!this.models) {
2864
+ throw new ConfigurationError(
2865
+ "Models are required. Use .withModels({ Transaction, Subscription })"
2866
+ );
2867
+ }
2868
+ if (!this.models.Transaction) {
2869
+ throw new ConfigurationError(
2870
+ "Transaction model is required in models configuration"
2871
+ );
2872
+ }
2873
+ if (Object.keys(this.providers).length === 0) {
2874
+ throw new ConfigurationError(
2875
+ "At least one provider is required. Use .withProvider(name, provider)"
2876
+ );
2877
+ }
2878
+ const container = new Container();
2879
+ const defaultLogger = {
2880
+ debug: (msg, data) => this.options.debug && console.debug(`[Revenue] ${msg}`, data ?? ""),
2881
+ info: (msg, data) => console.info(`[Revenue] ${msg}`, data ?? ""),
2882
+ warn: (msg, data) => console.warn(`[Revenue] ${msg}`, data ?? ""),
2883
+ error: (msg, data) => console.error(`[Revenue] ${msg}`, data ?? "")
2884
+ };
2885
+ const resolvedOptions = {
2886
+ defaultCurrency: this.options.defaultCurrency ?? "USD",
2887
+ environment: this.options.environment ?? "development",
2888
+ debug: this.options.debug ?? false,
2889
+ retry: this.options.retry ?? { maxAttempts: 3 },
2890
+ idempotencyTtl: this.options.idempotencyTtl ?? 24 * 60 * 60 * 1e3,
2891
+ circuitBreaker: this.options.circuitBreaker ?? false,
2892
+ logger: this.options.logger ?? defaultLogger,
2893
+ commissionRate: this.options.commissionRate ?? 0,
2894
+ gatewayFeeRate: this.options.gatewayFeeRate ?? 0
2895
+ };
2896
+ const config = {
2897
+ defaultCurrency: resolvedOptions.defaultCurrency,
2898
+ commissionRate: resolvedOptions.commissionRate,
2899
+ gatewayFeeRate: resolvedOptions.gatewayFeeRate,
2900
+ categoryMappings: this.categoryMappings
2901
+ };
2902
+ container.singleton("models", this.models);
2903
+ container.singleton("providers", this.providers);
2904
+ container.singleton("hooks", this.hooks);
2905
+ container.singleton("config", config);
2906
+ const events = createEventBus();
2907
+ const pluginManager = new PluginManager();
2908
+ for (const plugin of this.plugins) {
2909
+ pluginManager.register(plugin);
2910
+ }
2911
+ const revenue = new Revenue(
2912
+ container,
2913
+ events,
2914
+ pluginManager,
2915
+ resolvedOptions,
2916
+ this.providers,
2917
+ config
2918
+ );
2919
+ const ctx = revenue.createContext();
2920
+ pluginManager.init(ctx).catch((err2) => {
2921
+ resolvedOptions.logger.error("Failed to initialize plugins", err2);
2922
+ });
2923
+ return revenue;
2924
+ }
2925
+ };
2926
+ function createRevenue(config) {
2927
+ let builder = Revenue.create(config.options);
2928
+ builder = builder.withModels(config.models);
2929
+ builder = builder.withProviders(config.providers);
2930
+ if (config.plugins) {
2931
+ builder = builder.withPlugins(config.plugins);
2932
+ }
2933
+ if (config.hooks) {
2934
+ builder = builder.withHooks(config.hooks);
2935
+ }
2936
+ return builder.build();
2937
+ }
2938
+
2939
+ export { AlreadyVerifiedError, ConfigurationError, Container, ERROR_CODES, EventBus, InvalidAmountError, InvalidStateTransitionError, MissingRequiredFieldError, ModelNotRegisteredError, NotFoundError, OperationError, PaymentIntentCreationError, PaymentVerificationError, PluginManager, ProviderCapabilityError, ProviderError, ProviderNotFoundError, RefundError, RefundNotSupportedError, Result, Revenue, RevenueBuilder, RevenueError, StateError, SubscriptionNotActiveError, SubscriptionNotFoundError, TransactionNotFoundError, ValidationError, all, auditPlugin, createEventBus, createRevenue, definePlugin, err, flatMap, isErr, isOk, isRetryable, isRevenueError, loggingPlugin, map, mapErr, match, metricsPlugin, ok, tryCatch, tryCatchSync, unwrap, unwrapOr };
2940
+ //# sourceMappingURL=index.js.map
2941
+ //# sourceMappingURL=index.js.map