@classytic/revenue 1.0.6 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +582 -633
  2. package/dist/application/services/index.d.mts +4 -0
  3. package/dist/application/services/index.mjs +3 -0
  4. package/dist/base-CsTlVQJe.d.mts +136 -0
  5. package/dist/base-DCoyIUj6.mjs +152 -0
  6. package/dist/category-resolver-DV83N8ok.mjs +284 -0
  7. package/dist/commission-split-BzB8cd39.mjs +485 -0
  8. package/dist/core/events.d.mts +294 -0
  9. package/dist/core/events.mjs +100 -0
  10. package/dist/core/index.d.mts +9 -0
  11. package/dist/core/index.mjs +8 -0
  12. package/dist/enums/index.d.mts +157 -0
  13. package/dist/enums/index.mjs +56 -0
  14. package/dist/errors-CorrWz7A.d.mts +787 -0
  15. package/dist/escrow.enums-CZGrrdg7.mjs +101 -0
  16. package/dist/escrow.enums-DwdLuuve.d.mts +78 -0
  17. package/dist/idempotency-DaYcUGY1.mjs +172 -0
  18. package/dist/index-Dsp7H5Wb.d.mts +471 -0
  19. package/dist/index.d.mts +9 -0
  20. package/dist/index.mjs +38 -0
  21. package/dist/infrastructure/plugins/index.d.mts +239 -0
  22. package/dist/infrastructure/plugins/index.mjs +345 -0
  23. package/dist/money-CvrDOijQ.mjs +271 -0
  24. package/dist/money-DPG8AtJ8.d.mts +112 -0
  25. package/dist/payment.enums-HAuAS9Pp.d.mts +70 -0
  26. package/dist/payment.enums-tEFVa-Xp.mjs +69 -0
  27. package/dist/plugin-BbK0OVHy.d.mts +327 -0
  28. package/dist/plugin-Cd_V04Em.mjs +210 -0
  29. package/dist/providers/index.d.mts +3 -0
  30. package/dist/providers/index.mjs +3 -0
  31. package/dist/reconciliation/index.d.mts +193 -0
  32. package/dist/reconciliation/index.mjs +192 -0
  33. package/dist/retry-HHCOXYdn.d.mts +186 -0
  34. package/dist/revenue-9scqKSef.mjs +553 -0
  35. package/dist/schemas/index.d.mts +2665 -0
  36. package/dist/schemas/index.mjs +717 -0
  37. package/dist/schemas/validation.d.mts +375 -0
  38. package/dist/schemas/validation.mjs +325 -0
  39. package/dist/settlement.enums-DFhkqZEY.d.mts +132 -0
  40. package/dist/settlement.schema-D5uWB5tP.d.mts +344 -0
  41. package/dist/settlement.service-BxuiHpNC.d.mts +594 -0
  42. package/dist/settlement.service-CUxbUTzT.mjs +2510 -0
  43. package/dist/split.enums-BrjabxIX.mjs +86 -0
  44. package/dist/split.enums-DmskfLOM.d.mts +43 -0
  45. package/dist/tax-BoCt5cEd.d.mts +61 -0
  46. package/dist/tax-EQ15DO81.mjs +162 -0
  47. package/dist/transaction.enums-pCyMFT4Z.mjs +96 -0
  48. package/dist/utils/index.d.mts +428 -0
  49. package/dist/utils/index.mjs +346 -0
  50. package/package.json +48 -33
  51. package/dist/actions-Ctf2XUL-.d.ts +0 -519
  52. package/dist/core/index.d.ts +0 -890
  53. package/dist/core/index.js +0 -3005
  54. package/dist/core/index.js.map +0 -1
  55. package/dist/enums/index.d.ts +0 -138
  56. package/dist/enums/index.js +0 -263
  57. package/dist/enums/index.js.map +0 -1
  58. package/dist/index-BnEXsnLJ.d.ts +0 -378
  59. package/dist/index-C5SsOrV0.d.ts +0 -534
  60. package/dist/index.d.ts +0 -43
  61. package/dist/index.js +0 -4498
  62. package/dist/index.js.map +0 -1
  63. package/dist/payment.enums-B_RwB8iR.d.ts +0 -184
  64. package/dist/providers/index.d.ts +0 -132
  65. package/dist/providers/index.js +0 -122
  66. package/dist/providers/index.js.map +0 -1
  67. package/dist/retry-80lBCmSe.d.ts +0 -234
  68. package/dist/schemas/index.d.ts +0 -1051
  69. package/dist/schemas/index.js +0 -627
  70. package/dist/schemas/index.js.map +0 -1
  71. package/dist/schemas/validation.d.ts +0 -384
  72. package/dist/schemas/validation.js +0 -302
  73. package/dist/schemas/validation.js.map +0 -1
  74. package/dist/services/index.d.ts +0 -3
  75. package/dist/services/index.js +0 -1702
  76. package/dist/services/index.js.map +0 -1
  77. package/dist/split.schema-DLVF3XBI.d.ts +0 -1122
  78. package/dist/transaction.enums-7uBnuswI.d.ts +0 -87
  79. package/dist/utils/index.d.ts +0 -24
  80. package/dist/utils/index.js +0 -1140
  81. package/dist/utils/index.js.map +0 -1
@@ -1,3005 +0,0 @@
1
- import { nanoid } from 'nanoid';
2
-
3
- // @classytic/revenue - Enterprise Revenue Management System
4
-
5
-
6
- // src/core/container.ts
7
- var Container = class _Container {
8
- _services;
9
- _singletons;
10
- constructor() {
11
- this._services = /* @__PURE__ */ new Map();
12
- this._singletons = /* @__PURE__ */ new Map();
13
- }
14
- /**
15
- * Register a service
16
- * @param name - Service name
17
- * @param implementation - Service implementation or factory
18
- * @param options - Registration options
19
- */
20
- register(name, implementation, options = {}) {
21
- this._services.set(name, {
22
- implementation,
23
- singleton: options.singleton !== false,
24
- // Default to singleton
25
- factory: options.factory ?? false
26
- });
27
- return this;
28
- }
29
- /**
30
- * Register a singleton service
31
- * @param name - Service name
32
- * @param implementation - Service implementation
33
- */
34
- singleton(name, implementation) {
35
- return this.register(name, implementation, { singleton: true });
36
- }
37
- /**
38
- * Register a transient service (new instance each time)
39
- * @param name - Service name
40
- * @param factory - Factory function
41
- */
42
- transient(name, factory) {
43
- return this.register(name, factory, { singleton: false, factory: true });
44
- }
45
- /**
46
- * Get a service from the container
47
- * @param name - Service name
48
- * @returns Service instance
49
- */
50
- get(name) {
51
- if (this._singletons.has(name)) {
52
- return this._singletons.get(name);
53
- }
54
- const service = this._services.get(name);
55
- if (!service) {
56
- throw new Error(`Service "${name}" not registered in container`);
57
- }
58
- if (service.factory) {
59
- const factory = service.implementation;
60
- const instance2 = factory(this);
61
- if (service.singleton) {
62
- this._singletons.set(name, instance2);
63
- }
64
- return instance2;
65
- }
66
- const instance = service.implementation;
67
- if (service.singleton) {
68
- this._singletons.set(name, instance);
69
- }
70
- return instance;
71
- }
72
- /**
73
- * Check if service is registered
74
- * @param name - Service name
75
- */
76
- has(name) {
77
- return this._services.has(name);
78
- }
79
- /**
80
- * Get all registered service names
81
- */
82
- keys() {
83
- return Array.from(this._services.keys());
84
- }
85
- /**
86
- * Clear all services (useful for testing)
87
- */
88
- clear() {
89
- this._services.clear();
90
- this._singletons.clear();
91
- }
92
- /**
93
- * Create a child container (for scoped dependencies)
94
- */
95
- createScope() {
96
- const scope = new _Container();
97
- this._services.forEach((value, key) => {
98
- scope._services.set(key, value);
99
- });
100
- return scope;
101
- }
102
- };
103
-
104
- // src/core/events.ts
105
- var EventBus = class {
106
- handlers = /* @__PURE__ */ new Map();
107
- onceHandlers = /* @__PURE__ */ new Map();
108
- /**
109
- * Subscribe to an event
110
- */
111
- on(event, handler) {
112
- if (!this.handlers.has(event)) {
113
- this.handlers.set(event, /* @__PURE__ */ new Set());
114
- }
115
- this.handlers.get(event).add(handler);
116
- return () => this.off(event, handler);
117
- }
118
- /**
119
- * Subscribe to an event once
120
- */
121
- once(event, handler) {
122
- if (!this.onceHandlers.has(event)) {
123
- this.onceHandlers.set(event, /* @__PURE__ */ new Set());
124
- }
125
- this.onceHandlers.get(event).add(handler);
126
- return () => this.onceHandlers.get(event)?.delete(handler);
127
- }
128
- /**
129
- * Unsubscribe from an event
130
- */
131
- off(event, handler) {
132
- this.handlers.get(event)?.delete(handler);
133
- this.onceHandlers.get(event)?.delete(handler);
134
- }
135
- /**
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_TYPE_VALUES = Object.values(
1090
- TRANSACTION_TYPE
1091
- );
1092
- var TRANSACTION_STATUS = {
1093
- PENDING: "pending",
1094
- PAYMENT_INITIATED: "payment_initiated",
1095
- PROCESSING: "processing",
1096
- REQUIRES_ACTION: "requires_action",
1097
- VERIFIED: "verified",
1098
- COMPLETED: "completed",
1099
- FAILED: "failed",
1100
- CANCELLED: "cancelled",
1101
- EXPIRED: "expired",
1102
- REFUNDED: "refunded",
1103
- PARTIALLY_REFUNDED: "partially_refunded"
1104
- };
1105
- var TRANSACTION_STATUS_VALUES = Object.values(
1106
- TRANSACTION_STATUS
1107
- );
1108
- var LIBRARY_CATEGORIES = {
1109
- SUBSCRIPTION: "subscription",
1110
- PURCHASE: "purchase"
1111
- };
1112
- var LIBRARY_CATEGORY_VALUES = Object.values(
1113
- LIBRARY_CATEGORIES
1114
- );
1115
- new Set(TRANSACTION_TYPE_VALUES);
1116
- new Set(
1117
- TRANSACTION_STATUS_VALUES
1118
- );
1119
- new Set(LIBRARY_CATEGORY_VALUES);
1120
-
1121
- // src/utils/category-resolver.ts
1122
- function resolveCategory(entity, monetizationType, categoryMappings = {}) {
1123
- if (entity && categoryMappings[entity]) {
1124
- return categoryMappings[entity];
1125
- }
1126
- switch (monetizationType) {
1127
- case "subscription":
1128
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
1129
- // 'subscription'
1130
- case "purchase":
1131
- return LIBRARY_CATEGORIES.PURCHASE;
1132
- // 'purchase'
1133
- default:
1134
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
1135
- }
1136
- }
1137
-
1138
- // src/utils/commission.ts
1139
- function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
1140
- if (!commissionRate || commissionRate <= 0) {
1141
- return null;
1142
- }
1143
- if (amount < 0) {
1144
- throw new Error("Transaction amount cannot be negative");
1145
- }
1146
- if (commissionRate < 0 || commissionRate > 1) {
1147
- throw new Error("Commission rate must be between 0 and 1");
1148
- }
1149
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
1150
- throw new Error("Gateway fee rate must be between 0 and 1");
1151
- }
1152
- const grossAmount = Math.round(amount * commissionRate * 100) / 100;
1153
- const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
1154
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
1155
- return {
1156
- rate: commissionRate,
1157
- grossAmount,
1158
- gatewayFeeRate,
1159
- gatewayFeeAmount,
1160
- netAmount,
1161
- status: "pending"
1162
- };
1163
- }
1164
- function reverseCommission(originalCommission, originalAmount, refundAmount) {
1165
- if (!originalCommission?.netAmount) {
1166
- return null;
1167
- }
1168
- const refundRatio = refundAmount / originalAmount;
1169
- const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
1170
- const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
1171
- const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
1172
- return {
1173
- rate: originalCommission.rate,
1174
- grossAmount: reversedGrossAmount,
1175
- gatewayFeeRate: originalCommission.gatewayFeeRate,
1176
- gatewayFeeAmount: reversedGatewayFee,
1177
- netAmount: reversedNetAmount,
1178
- status: "waived"
1179
- // Commission waived due to refund
1180
- };
1181
- }
1182
-
1183
- // src/enums/monetization.enums.ts
1184
- var MONETIZATION_TYPES = {
1185
- FREE: "free",
1186
- PURCHASE: "purchase",
1187
- SUBSCRIPTION: "subscription"
1188
- };
1189
- var MONETIZATION_TYPE_VALUES = Object.values(
1190
- MONETIZATION_TYPES
1191
- );
1192
- new Set(MONETIZATION_TYPE_VALUES);
1193
-
1194
- // src/services/monetization.service.ts
1195
- var MonetizationService = class {
1196
- models;
1197
- providers;
1198
- config;
1199
- hooks;
1200
- logger;
1201
- constructor(container) {
1202
- this.models = container.get("models");
1203
- this.providers = container.get("providers");
1204
- this.config = container.get("config");
1205
- this.hooks = container.get("hooks");
1206
- this.logger = container.get("logger");
1207
- }
1208
- /**
1209
- * Create a new monetization (purchase, subscription, or free item)
1210
- *
1211
- * @param params - Monetization parameters
1212
- *
1213
- * @example
1214
- * // One-time purchase
1215
- * await revenue.monetization.create({
1216
- * data: {
1217
- * organizationId: '...',
1218
- * customerId: '...',
1219
- * referenceId: order._id,
1220
- * referenceModel: 'Order',
1221
- * },
1222
- * planKey: 'one_time',
1223
- * monetizationType: 'purchase',
1224
- * gateway: 'bkash',
1225
- * amount: 1500,
1226
- * });
1227
- *
1228
- * // Recurring subscription
1229
- * await revenue.monetization.create({
1230
- * data: {
1231
- * organizationId: '...',
1232
- * customerId: '...',
1233
- * referenceId: subscription._id,
1234
- * referenceModel: 'Subscription',
1235
- * },
1236
- * planKey: 'monthly',
1237
- * monetizationType: 'subscription',
1238
- * gateway: 'stripe',
1239
- * amount: 2000,
1240
- * });
1241
- *
1242
- * @returns Result with subscription, transaction, and paymentIntent
1243
- */
1244
- async create(params) {
1245
- const {
1246
- data,
1247
- planKey,
1248
- amount,
1249
- currency = "BDT",
1250
- gateway = "manual",
1251
- entity = null,
1252
- monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
1253
- paymentData,
1254
- metadata = {},
1255
- idempotencyKey = null
1256
- } = params;
1257
- if (!planKey) {
1258
- throw new MissingRequiredFieldError("planKey");
1259
- }
1260
- if (amount < 0) {
1261
- throw new InvalidAmountError(amount);
1262
- }
1263
- const isFree = amount === 0;
1264
- const provider = this.providers[gateway];
1265
- if (!provider) {
1266
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1267
- }
1268
- let paymentIntent = null;
1269
- let transaction = null;
1270
- if (!isFree) {
1271
- try {
1272
- paymentIntent = await provider.createIntent({
1273
- amount,
1274
- currency,
1275
- metadata: {
1276
- ...metadata,
1277
- type: "subscription",
1278
- planKey
1279
- }
1280
- });
1281
- } catch (error) {
1282
- throw new PaymentIntentCreationError(gateway, error);
1283
- }
1284
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
1285
- const transactionType = this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_TYPE.INCOME;
1286
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
1287
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
1288
- const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
1289
- const TransactionModel = this.models.Transaction;
1290
- transaction = await TransactionModel.create({
1291
- organizationId: data.organizationId,
1292
- customerId: data.customerId ?? null,
1293
- amount,
1294
- currency,
1295
- category,
1296
- type: transactionType,
1297
- method: paymentData?.method ?? "manual",
1298
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1299
- gateway: {
1300
- type: gateway,
1301
- sessionId: paymentIntent.sessionId,
1302
- paymentIntentId: paymentIntent.paymentIntentId,
1303
- provider: paymentIntent.provider,
1304
- metadata: paymentIntent.metadata
1305
- },
1306
- paymentDetails: {
1307
- provider: gateway,
1308
- ...paymentData
1309
- },
1310
- ...commission && { commission },
1311
- // Only include if commission exists
1312
- // Polymorphic reference (top-level, not metadata)
1313
- ...data.referenceId && { referenceId: data.referenceId },
1314
- ...data.referenceModel && { referenceModel: data.referenceModel },
1315
- metadata: {
1316
- ...metadata,
1317
- planKey,
1318
- entity,
1319
- monetizationType,
1320
- paymentIntentId: paymentIntent.id
1321
- },
1322
- idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
1323
- });
1324
- }
1325
- let subscription = null;
1326
- if (this.models.Subscription) {
1327
- const SubscriptionModel = this.models.Subscription;
1328
- const subscriptionData = {
1329
- organizationId: data.organizationId,
1330
- customerId: data.customerId ?? null,
1331
- planKey,
1332
- amount,
1333
- currency,
1334
- status: isFree ? "active" : "pending",
1335
- isActive: isFree,
1336
- gateway,
1337
- transactionId: transaction?._id ?? null,
1338
- paymentIntentId: paymentIntent?.id ?? null,
1339
- metadata: {
1340
- ...metadata,
1341
- isFree,
1342
- entity,
1343
- monetizationType
1344
- },
1345
- ...data
1346
- };
1347
- delete subscriptionData.referenceId;
1348
- delete subscriptionData.referenceModel;
1349
- subscription = await SubscriptionModel.create(subscriptionData);
1350
- }
1351
- const eventData = {
1352
- subscription,
1353
- transaction,
1354
- paymentIntent,
1355
- isFree,
1356
- monetizationType
1357
- };
1358
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
1359
- this._triggerHook("purchase.created", eventData);
1360
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
1361
- this._triggerHook("subscription.created", eventData);
1362
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
1363
- this._triggerHook("free.created", eventData);
1364
- }
1365
- this._triggerHook("monetization.created", eventData);
1366
- return {
1367
- subscription,
1368
- transaction,
1369
- paymentIntent
1370
- };
1371
- }
1372
- /**
1373
- * Activate subscription after payment verification
1374
- *
1375
- * @param subscriptionId - Subscription ID or transaction ID
1376
- * @param options - Activation options
1377
- * @returns Updated subscription
1378
- */
1379
- async activate(subscriptionId, options = {}) {
1380
- const { timestamp = /* @__PURE__ */ new Date() } = options;
1381
- if (!this.models.Subscription) {
1382
- throw new ModelNotRegisteredError("Subscription");
1383
- }
1384
- const SubscriptionModel = this.models.Subscription;
1385
- const subscription = await SubscriptionModel.findById(subscriptionId);
1386
- if (!subscription) {
1387
- throw new SubscriptionNotFoundError(subscriptionId);
1388
- }
1389
- if (subscription.isActive) {
1390
- this.logger.warn("Subscription already active", { subscriptionId });
1391
- return subscription;
1392
- }
1393
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
1394
- subscription.isActive = true;
1395
- subscription.status = "active";
1396
- subscription.startDate = timestamp;
1397
- subscription.endDate = periodEnd;
1398
- subscription.activatedAt = timestamp;
1399
- await subscription.save();
1400
- this._triggerHook("subscription.activated", {
1401
- subscription,
1402
- activatedAt: timestamp
1403
- });
1404
- return subscription;
1405
- }
1406
- /**
1407
- * Renew subscription
1408
- *
1409
- * @param subscriptionId - Subscription ID
1410
- * @param params - Renewal parameters
1411
- * @returns { subscription, transaction, paymentIntent }
1412
- */
1413
- async renew(subscriptionId, params = {}) {
1414
- const {
1415
- gateway = "manual",
1416
- entity = null,
1417
- paymentData,
1418
- metadata = {},
1419
- idempotencyKey = null
1420
- } = params;
1421
- if (!this.models.Subscription) {
1422
- throw new ModelNotRegisteredError("Subscription");
1423
- }
1424
- const SubscriptionModel = this.models.Subscription;
1425
- const subscription = await SubscriptionModel.findById(subscriptionId);
1426
- if (!subscription) {
1427
- throw new SubscriptionNotFoundError(subscriptionId);
1428
- }
1429
- if (subscription.amount === 0) {
1430
- throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
1431
- }
1432
- const provider = this.providers[gateway];
1433
- if (!provider) {
1434
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1435
- }
1436
- let paymentIntent = null;
1437
- try {
1438
- paymentIntent = await provider.createIntent({
1439
- amount: subscription.amount,
1440
- currency: subscription.currency ?? "BDT",
1441
- metadata: {
1442
- ...metadata,
1443
- type: "subscription_renewal",
1444
- subscriptionId: subscription._id.toString()
1445
- }
1446
- });
1447
- } catch (error) {
1448
- this.logger.error("Failed to create payment intent for renewal:", error);
1449
- throw new PaymentIntentCreationError(gateway, error);
1450
- }
1451
- const effectiveEntity = entity ?? subscription.metadata?.entity;
1452
- const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
1453
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
1454
- const transactionType = this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_TYPE.INCOME;
1455
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
1456
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
1457
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
1458
- const TransactionModel = this.models.Transaction;
1459
- const transaction = await TransactionModel.create({
1460
- organizationId: subscription.organizationId,
1461
- customerId: subscription.customerId,
1462
- amount: subscription.amount,
1463
- currency: subscription.currency ?? "BDT",
1464
- category,
1465
- type: transactionType,
1466
- method: paymentData?.method ?? "manual",
1467
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1468
- gateway: {
1469
- type: gateway,
1470
- sessionId: paymentIntent.sessionId,
1471
- paymentIntentId: paymentIntent.paymentIntentId,
1472
- provider: paymentIntent.provider,
1473
- metadata: paymentIntent.metadata
1474
- },
1475
- paymentDetails: {
1476
- provider: gateway,
1477
- ...paymentData
1478
- },
1479
- ...commission && { commission },
1480
- // Only include if commission exists
1481
- // Polymorphic reference to subscription
1482
- referenceId: subscription._id,
1483
- referenceModel: "Subscription",
1484
- metadata: {
1485
- ...metadata,
1486
- subscriptionId: subscription._id.toString(),
1487
- // Keep for backward compat
1488
- entity: effectiveEntity,
1489
- monetizationType: effectiveMonetizationType,
1490
- isRenewal: true,
1491
- paymentIntentId: paymentIntent.id
1492
- },
1493
- idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
1494
- });
1495
- subscription.status = "pending_renewal";
1496
- subscription.renewalTransactionId = transaction._id;
1497
- subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
1498
- await subscription.save();
1499
- this._triggerHook("subscription.renewed", {
1500
- subscription,
1501
- transaction,
1502
- paymentIntent,
1503
- renewalCount: subscription.renewalCount
1504
- });
1505
- return {
1506
- subscription,
1507
- transaction,
1508
- paymentIntent
1509
- };
1510
- }
1511
- /**
1512
- * Cancel subscription
1513
- *
1514
- * @param subscriptionId - Subscription ID
1515
- * @param options - Cancellation options
1516
- * @returns Updated subscription
1517
- */
1518
- async cancel(subscriptionId, options = {}) {
1519
- const { immediate = false, reason = null } = options;
1520
- if (!this.models.Subscription) {
1521
- throw new ModelNotRegisteredError("Subscription");
1522
- }
1523
- const SubscriptionModel = this.models.Subscription;
1524
- const subscription = await SubscriptionModel.findById(subscriptionId);
1525
- if (!subscription) {
1526
- throw new SubscriptionNotFoundError(subscriptionId);
1527
- }
1528
- const now = /* @__PURE__ */ new Date();
1529
- if (immediate) {
1530
- subscription.isActive = false;
1531
- subscription.status = "cancelled";
1532
- subscription.canceledAt = now;
1533
- subscription.cancellationReason = reason;
1534
- } else {
1535
- subscription.cancelAt = subscription.endDate ?? now;
1536
- subscription.cancellationReason = reason;
1537
- }
1538
- await subscription.save();
1539
- this._triggerHook("subscription.cancelled", {
1540
- subscription,
1541
- immediate,
1542
- reason,
1543
- canceledAt: immediate ? now : subscription.cancelAt
1544
- });
1545
- return subscription;
1546
- }
1547
- /**
1548
- * Pause subscription
1549
- *
1550
- * @param subscriptionId - Subscription ID
1551
- * @param options - Pause options
1552
- * @returns Updated subscription
1553
- */
1554
- async pause(subscriptionId, options = {}) {
1555
- const { reason = null } = options;
1556
- if (!this.models.Subscription) {
1557
- throw new ModelNotRegisteredError("Subscription");
1558
- }
1559
- const SubscriptionModel = this.models.Subscription;
1560
- const subscription = await SubscriptionModel.findById(subscriptionId);
1561
- if (!subscription) {
1562
- throw new SubscriptionNotFoundError(subscriptionId);
1563
- }
1564
- if (!subscription.isActive) {
1565
- throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
1566
- }
1567
- const pausedAt = /* @__PURE__ */ new Date();
1568
- subscription.isActive = false;
1569
- subscription.status = "paused";
1570
- subscription.pausedAt = pausedAt;
1571
- subscription.pauseReason = reason;
1572
- await subscription.save();
1573
- this._triggerHook("subscription.paused", {
1574
- subscription,
1575
- reason,
1576
- pausedAt
1577
- });
1578
- return subscription;
1579
- }
1580
- /**
1581
- * Resume subscription
1582
- *
1583
- * @param subscriptionId - Subscription ID
1584
- * @param options - Resume options
1585
- * @returns Updated subscription
1586
- */
1587
- async resume(subscriptionId, options = {}) {
1588
- const { extendPeriod = false } = options;
1589
- if (!this.models.Subscription) {
1590
- throw new ModelNotRegisteredError("Subscription");
1591
- }
1592
- const SubscriptionModel = this.models.Subscription;
1593
- const subscription = await SubscriptionModel.findById(subscriptionId);
1594
- if (!subscription) {
1595
- throw new SubscriptionNotFoundError(subscriptionId);
1596
- }
1597
- if (!subscription.pausedAt) {
1598
- throw new InvalidStateTransitionError(
1599
- "resume",
1600
- "paused",
1601
- subscription.status,
1602
- "Only paused subscriptions can be resumed"
1603
- );
1604
- }
1605
- const now = /* @__PURE__ */ new Date();
1606
- const pausedAt = new Date(subscription.pausedAt);
1607
- const pauseDuration = now.getTime() - pausedAt.getTime();
1608
- subscription.isActive = true;
1609
- subscription.status = "active";
1610
- subscription.pausedAt = null;
1611
- subscription.pauseReason = null;
1612
- if (extendPeriod && subscription.endDate) {
1613
- const currentEnd = new Date(subscription.endDate);
1614
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1615
- }
1616
- await subscription.save();
1617
- this._triggerHook("subscription.resumed", {
1618
- subscription,
1619
- extendPeriod,
1620
- pauseDuration,
1621
- resumedAt: now
1622
- });
1623
- return subscription;
1624
- }
1625
- /**
1626
- * List subscriptions with filters
1627
- *
1628
- * @param filters - Query filters
1629
- * @param options - Query options (limit, skip, sort)
1630
- * @returns Subscriptions
1631
- */
1632
- async list(filters = {}, options = {}) {
1633
- if (!this.models.Subscription) {
1634
- throw new ModelNotRegisteredError("Subscription");
1635
- }
1636
- const SubscriptionModel = this.models.Subscription;
1637
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1638
- const subscriptions = await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
1639
- return subscriptions;
1640
- }
1641
- /**
1642
- * Get subscription by ID
1643
- *
1644
- * @param subscriptionId - Subscription ID
1645
- * @returns Subscription
1646
- */
1647
- async get(subscriptionId) {
1648
- if (!this.models.Subscription) {
1649
- throw new ModelNotRegisteredError("Subscription");
1650
- }
1651
- const SubscriptionModel = this.models.Subscription;
1652
- const subscription = await SubscriptionModel.findById(subscriptionId);
1653
- if (!subscription) {
1654
- throw new SubscriptionNotFoundError(subscriptionId);
1655
- }
1656
- return subscription;
1657
- }
1658
- /**
1659
- * Calculate period end date based on plan key
1660
- * @private
1661
- */
1662
- _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
1663
- const start = new Date(startDate);
1664
- const end = new Date(start);
1665
- switch (planKey) {
1666
- case "monthly":
1667
- end.setMonth(end.getMonth() + 1);
1668
- break;
1669
- case "quarterly":
1670
- end.setMonth(end.getMonth() + 3);
1671
- break;
1672
- case "yearly":
1673
- end.setFullYear(end.getFullYear() + 1);
1674
- break;
1675
- default:
1676
- end.setDate(end.getDate() + 30);
1677
- }
1678
- return end;
1679
- }
1680
- /**
1681
- * Trigger event hook (fire-and-forget, non-blocking)
1682
- * @private
1683
- */
1684
- _triggerHook(event, data) {
1685
- triggerHook(this.hooks, event, data, this.logger);
1686
- }
1687
- };
1688
-
1689
- // src/services/payment.service.ts
1690
- var PaymentService = class {
1691
- models;
1692
- providers;
1693
- config;
1694
- hooks;
1695
- logger;
1696
- constructor(container) {
1697
- this.models = container.get("models");
1698
- this.providers = container.get("providers");
1699
- this.config = container.get("config");
1700
- this.hooks = container.get("hooks");
1701
- this.logger = container.get("logger");
1702
- }
1703
- /**
1704
- * Verify a payment
1705
- *
1706
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1707
- * @param options - Verification options
1708
- * @returns { transaction, status }
1709
- */
1710
- async verify(paymentIntentId, options = {}) {
1711
- const { verifiedBy = null } = options;
1712
- const TransactionModel = this.models.Transaction;
1713
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1714
- if (!transaction) {
1715
- throw new TransactionNotFoundError(paymentIntentId);
1716
- }
1717
- if (transaction.status === "verified" || transaction.status === "completed") {
1718
- throw new AlreadyVerifiedError(transaction._id.toString());
1719
- }
1720
- const gatewayType = transaction.gateway?.type ?? "manual";
1721
- const provider = this.providers[gatewayType];
1722
- if (!provider) {
1723
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1724
- }
1725
- let paymentResult = null;
1726
- try {
1727
- paymentResult = await provider.verifyPayment(paymentIntentId);
1728
- } catch (error) {
1729
- this.logger.error("Payment verification failed:", error);
1730
- transaction.status = "failed";
1731
- transaction.failureReason = error.message;
1732
- transaction.metadata = {
1733
- ...transaction.metadata,
1734
- verificationError: error.message,
1735
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
1736
- };
1737
- await transaction.save();
1738
- this._triggerHook("payment.failed", {
1739
- transaction,
1740
- error: error.message,
1741
- provider: gatewayType,
1742
- paymentIntentId
1743
- });
1744
- throw new PaymentVerificationError(paymentIntentId, error.message);
1745
- }
1746
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
1747
- throw new ValidationError(
1748
- `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
1749
- { expected: transaction.amount, actual: paymentResult.amount }
1750
- );
1751
- }
1752
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
1753
- throw new ValidationError(
1754
- `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
1755
- { expected: transaction.currency, actual: paymentResult.currency }
1756
- );
1757
- }
1758
- transaction.status = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1759
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1760
- transaction.verifiedBy = verifiedBy;
1761
- transaction.gateway = {
1762
- ...transaction.gateway,
1763
- type: transaction.gateway?.type ?? "manual",
1764
- verificationData: paymentResult.metadata
1765
- };
1766
- await transaction.save();
1767
- this._triggerHook("payment.verified", {
1768
- transaction,
1769
- paymentResult,
1770
- verifiedBy
1771
- });
1772
- return {
1773
- transaction,
1774
- paymentResult,
1775
- status: transaction.status
1776
- };
1777
- }
1778
- /**
1779
- * Get payment status
1780
- *
1781
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1782
- * @returns { transaction, status }
1783
- */
1784
- async getStatus(paymentIntentId) {
1785
- const TransactionModel = this.models.Transaction;
1786
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1787
- if (!transaction) {
1788
- throw new TransactionNotFoundError(paymentIntentId);
1789
- }
1790
- const gatewayType = transaction.gateway?.type ?? "manual";
1791
- const provider = this.providers[gatewayType];
1792
- if (!provider) {
1793
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1794
- }
1795
- let paymentResult = null;
1796
- try {
1797
- paymentResult = await provider.getStatus(paymentIntentId);
1798
- } catch (error) {
1799
- this.logger.warn("Failed to get payment status from provider:", error);
1800
- return {
1801
- transaction,
1802
- status: transaction.status,
1803
- provider: gatewayType
1804
- };
1805
- }
1806
- return {
1807
- transaction,
1808
- paymentResult,
1809
- status: paymentResult.status,
1810
- provider: gatewayType
1811
- };
1812
- }
1813
- /**
1814
- * Refund a payment
1815
- *
1816
- * @param paymentId - Payment intent ID, session ID, or transaction ID
1817
- * @param amount - Amount to refund (optional, full refund if not provided)
1818
- * @param options - Refund options
1819
- * @returns { transaction, refundResult }
1820
- */
1821
- async refund(paymentId, amount = null, options = {}) {
1822
- const { reason = null } = options;
1823
- const TransactionModel = this.models.Transaction;
1824
- const transaction = await this._findTransaction(TransactionModel, paymentId);
1825
- if (!transaction) {
1826
- throw new TransactionNotFoundError(paymentId);
1827
- }
1828
- if (transaction.status !== "verified" && transaction.status !== "completed") {
1829
- throw new RefundError(transaction._id.toString(), "Only verified/completed transactions can be refunded");
1830
- }
1831
- const gatewayType = transaction.gateway?.type ?? "manual";
1832
- const provider = this.providers[gatewayType];
1833
- if (!provider) {
1834
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1835
- }
1836
- const capabilities = provider.getCapabilities();
1837
- if (!capabilities.supportsRefunds) {
1838
- throw new RefundNotSupportedError(gatewayType);
1839
- }
1840
- const refundedSoFar = transaction.refundedAmount ?? 0;
1841
- const refundableAmount = transaction.amount - refundedSoFar;
1842
- const refundAmount = amount ?? refundableAmount;
1843
- if (refundAmount <= 0) {
1844
- throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1845
- }
1846
- if (refundAmount > refundableAmount) {
1847
- throw new ValidationError(
1848
- `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
1849
- { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
1850
- );
1851
- }
1852
- let refundResult;
1853
- try {
1854
- refundResult = await provider.refund(paymentId, refundAmount, { reason: reason ?? void 0 });
1855
- } catch (error) {
1856
- this.logger.error("Refund failed:", error);
1857
- throw new RefundError(paymentId, error.message);
1858
- }
1859
- const refundTransactionType = this.config.transactionTypeMapping?.refund ?? TRANSACTION_TYPE.EXPENSE;
1860
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1861
- const refundTransaction = await TransactionModel.create({
1862
- organizationId: transaction.organizationId,
1863
- customerId: transaction.customerId,
1864
- amount: refundAmount,
1865
- currency: transaction.currency,
1866
- category: transaction.category,
1867
- type: refundTransactionType,
1868
- // EXPENSE - money going out
1869
- method: transaction.method ?? "manual",
1870
- status: "completed",
1871
- gateway: {
1872
- type: transaction.gateway?.type ?? "manual",
1873
- paymentIntentId: refundResult.id,
1874
- provider: refundResult.provider
1875
- },
1876
- paymentDetails: transaction.paymentDetails,
1877
- ...refundCommission && { commission: refundCommission },
1878
- // Reversed commission
1879
- // Polymorphic reference (copy from original transaction)
1880
- ...transaction.referenceId && { referenceId: transaction.referenceId },
1881
- ...transaction.referenceModel && { referenceModel: transaction.referenceModel },
1882
- metadata: {
1883
- ...transaction.metadata,
1884
- isRefund: true,
1885
- originalTransactionId: transaction._id.toString(),
1886
- refundReason: reason,
1887
- refundResult: refundResult.metadata
1888
- },
1889
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1890
- });
1891
- const isPartialRefund = refundAmount < transaction.amount;
1892
- transaction.status = isPartialRefund ? "partially_refunded" : "refunded";
1893
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1894
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1895
- transaction.metadata = {
1896
- ...transaction.metadata,
1897
- refundTransactionId: refundTransaction._id.toString(),
1898
- refundReason: reason
1899
- };
1900
- await transaction.save();
1901
- this._triggerHook("payment.refunded", {
1902
- transaction,
1903
- refundTransaction,
1904
- refundResult,
1905
- refundAmount,
1906
- reason,
1907
- isPartialRefund
1908
- });
1909
- return {
1910
- transaction,
1911
- refundTransaction,
1912
- refundResult,
1913
- status: transaction.status
1914
- };
1915
- }
1916
- /**
1917
- * Handle webhook from payment provider
1918
- *
1919
- * @param provider - Provider name
1920
- * @param payload - Webhook payload
1921
- * @param headers - Request headers
1922
- * @returns { event, transaction }
1923
- */
1924
- async handleWebhook(providerName, payload, headers = {}) {
1925
- const provider = this.providers[providerName];
1926
- if (!provider) {
1927
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1928
- }
1929
- const capabilities = provider.getCapabilities();
1930
- if (!capabilities.supportsWebhooks) {
1931
- throw new ProviderCapabilityError(providerName, "webhooks");
1932
- }
1933
- let webhookEvent;
1934
- try {
1935
- webhookEvent = await provider.handleWebhook(payload, headers);
1936
- } catch (error) {
1937
- this.logger.error("Webhook processing failed:", error);
1938
- throw new ProviderError(
1939
- `Webhook processing failed for ${providerName}: ${error.message}`,
1940
- "WEBHOOK_PROCESSING_FAILED",
1941
- { retryable: false }
1942
- );
1943
- }
1944
- if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) {
1945
- throw new ValidationError(
1946
- `Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`,
1947
- { provider: providerName, eventType: webhookEvent?.type }
1948
- );
1949
- }
1950
- const TransactionModel = this.models.Transaction;
1951
- let transaction = null;
1952
- if (webhookEvent.data.sessionId) {
1953
- transaction = await TransactionModel.findOne({
1954
- "gateway.sessionId": webhookEvent.data.sessionId
1955
- });
1956
- }
1957
- if (!transaction && webhookEvent.data.paymentIntentId) {
1958
- transaction = await TransactionModel.findOne({
1959
- "gateway.paymentIntentId": webhookEvent.data.paymentIntentId
1960
- });
1961
- }
1962
- if (!transaction) {
1963
- this.logger.warn("Transaction not found for webhook event", {
1964
- provider: providerName,
1965
- eventId: webhookEvent.id,
1966
- sessionId: webhookEvent.data.sessionId,
1967
- paymentIntentId: webhookEvent.data.paymentIntentId
1968
- });
1969
- throw new TransactionNotFoundError(
1970
- webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown"
1971
- );
1972
- }
1973
- if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) {
1974
- transaction.gateway = {
1975
- ...transaction.gateway,
1976
- type: transaction.gateway?.type ?? "manual",
1977
- sessionId: webhookEvent.data.sessionId
1978
- };
1979
- }
1980
- if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) {
1981
- transaction.gateway = {
1982
- ...transaction.gateway,
1983
- type: transaction.gateway?.type ?? "manual",
1984
- paymentIntentId: webhookEvent.data.paymentIntentId
1985
- };
1986
- }
1987
- if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
1988
- this.logger.warn("Webhook already processed", {
1989
- transactionId: transaction._id,
1990
- eventId: webhookEvent.id
1991
- });
1992
- return {
1993
- event: webhookEvent,
1994
- transaction,
1995
- status: "already_processed"
1996
- };
1997
- }
1998
- transaction.webhook = {
1999
- eventId: webhookEvent.id,
2000
- eventType: webhookEvent.type,
2001
- receivedAt: /* @__PURE__ */ new Date(),
2002
- processedAt: /* @__PURE__ */ new Date(),
2003
- data: webhookEvent.data
2004
- };
2005
- if (webhookEvent.type === "payment.succeeded") {
2006
- transaction.status = "verified";
2007
- transaction.verifiedAt = webhookEvent.createdAt;
2008
- } else if (webhookEvent.type === "payment.failed") {
2009
- transaction.status = "failed";
2010
- } else if (webhookEvent.type === "refund.succeeded") {
2011
- transaction.status = "refunded";
2012
- transaction.refundedAt = webhookEvent.createdAt;
2013
- }
2014
- await transaction.save();
2015
- this._triggerHook(`payment.webhook.${webhookEvent.type}`, {
2016
- event: webhookEvent,
2017
- transaction
2018
- });
2019
- return {
2020
- event: webhookEvent,
2021
- transaction,
2022
- status: "processed"
2023
- };
2024
- }
2025
- /**
2026
- * List payments/transactions with filters
2027
- *
2028
- * @param filters - Query filters
2029
- * @param options - Query options (limit, skip, sort)
2030
- * @returns Transactions
2031
- */
2032
- async list(filters = {}, options = {}) {
2033
- const TransactionModel = this.models.Transaction;
2034
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
2035
- const transactions = await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
2036
- return transactions;
2037
- }
2038
- /**
2039
- * Get payment/transaction by ID
2040
- *
2041
- * @param transactionId - Transaction ID
2042
- * @returns Transaction
2043
- */
2044
- async get(transactionId) {
2045
- const TransactionModel = this.models.Transaction;
2046
- const transaction = await TransactionModel.findById(transactionId);
2047
- if (!transaction) {
2048
- throw new TransactionNotFoundError(transactionId);
2049
- }
2050
- return transaction;
2051
- }
2052
- /**
2053
- * Get provider instance
2054
- *
2055
- * @param providerName - Provider name
2056
- * @returns Provider instance
2057
- */
2058
- getProvider(providerName) {
2059
- const provider = this.providers[providerName];
2060
- if (!provider) {
2061
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
2062
- }
2063
- return provider;
2064
- }
2065
- /**
2066
- * Trigger event hook (fire-and-forget, non-blocking)
2067
- * @private
2068
- */
2069
- _triggerHook(event, data) {
2070
- triggerHook(this.hooks, event, data, this.logger);
2071
- }
2072
- /**
2073
- * Find transaction by sessionId, paymentIntentId, or transaction ID
2074
- * @private
2075
- */
2076
- async _findTransaction(TransactionModel, identifier) {
2077
- let transaction = await TransactionModel.findOne({
2078
- "gateway.sessionId": identifier
2079
- });
2080
- if (!transaction) {
2081
- transaction = await TransactionModel.findOne({
2082
- "gateway.paymentIntentId": identifier
2083
- });
2084
- }
2085
- if (!transaction) {
2086
- transaction = await TransactionModel.findById(identifier);
2087
- }
2088
- return transaction;
2089
- }
2090
- };
2091
-
2092
- // src/services/transaction.service.ts
2093
- var TransactionService = class {
2094
- models;
2095
- hooks;
2096
- logger;
2097
- constructor(container) {
2098
- this.models = container.get("models");
2099
- this.hooks = container.get("hooks");
2100
- this.logger = container.get("logger");
2101
- }
2102
- /**
2103
- * Get transaction by ID
2104
- *
2105
- * @param transactionId - Transaction ID
2106
- * @returns Transaction
2107
- */
2108
- async get(transactionId) {
2109
- const TransactionModel = this.models.Transaction;
2110
- const transaction = await TransactionModel.findById(transactionId);
2111
- if (!transaction) {
2112
- throw new TransactionNotFoundError(transactionId);
2113
- }
2114
- return transaction;
2115
- }
2116
- /**
2117
- * List transactions with filters
2118
- *
2119
- * @param filters - Query filters
2120
- * @param options - Query options (limit, skip, sort, populate)
2121
- * @returns { transactions, total, page, limit }
2122
- */
2123
- async list(filters = {}, options = {}) {
2124
- const TransactionModel = this.models.Transaction;
2125
- const {
2126
- limit = 50,
2127
- skip = 0,
2128
- page = null,
2129
- sort = { createdAt: -1 },
2130
- populate = []
2131
- } = options;
2132
- const actualSkip = page ? (page - 1) * limit : skip;
2133
- let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
2134
- if (populate.length > 0 && typeof query.populate === "function") {
2135
- populate.forEach((field) => {
2136
- query = query.populate(field);
2137
- });
2138
- }
2139
- const transactions = await query;
2140
- const model = TransactionModel;
2141
- const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
2142
- return {
2143
- transactions,
2144
- total,
2145
- page: page ?? Math.floor(actualSkip / limit) + 1,
2146
- limit,
2147
- pages: Math.ceil(total / limit)
2148
- };
2149
- }
2150
- /**
2151
- * Update transaction
2152
- *
2153
- * @param transactionId - Transaction ID
2154
- * @param updates - Fields to update
2155
- * @returns Updated transaction
2156
- */
2157
- async update(transactionId, updates) {
2158
- const TransactionModel = this.models.Transaction;
2159
- const model = TransactionModel;
2160
- let transaction;
2161
- if (typeof model.update === "function") {
2162
- transaction = await model.update(transactionId, updates);
2163
- } else if (typeof model.findByIdAndUpdate === "function") {
2164
- transaction = await model.findByIdAndUpdate(
2165
- transactionId,
2166
- { $set: updates },
2167
- { new: true }
2168
- );
2169
- } else {
2170
- throw new Error("Transaction model does not support update operations");
2171
- }
2172
- if (!transaction) {
2173
- throw new TransactionNotFoundError(transactionId);
2174
- }
2175
- this._triggerHook("transaction.updated", {
2176
- transaction,
2177
- updates
2178
- });
2179
- return transaction;
2180
- }
2181
- /**
2182
- * Trigger event hook (fire-and-forget, non-blocking)
2183
- * @private
2184
- */
2185
- _triggerHook(event, data) {
2186
- triggerHook(this.hooks, event, data, this.logger);
2187
- }
2188
- };
2189
-
2190
- // src/enums/escrow.enums.ts
2191
- var HOLD_STATUS = {
2192
- PENDING: "pending",
2193
- HELD: "held",
2194
- RELEASED: "released",
2195
- CANCELLED: "cancelled",
2196
- EXPIRED: "expired",
2197
- PARTIALLY_RELEASED: "partially_released"
2198
- };
2199
- var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
2200
- var RELEASE_REASON = {
2201
- PAYMENT_VERIFIED: "payment_verified",
2202
- MANUAL_RELEASE: "manual_release",
2203
- AUTO_RELEASE: "auto_release",
2204
- DISPUTE_RESOLVED: "dispute_resolved"
2205
- };
2206
- var RELEASE_REASON_VALUES = Object.values(
2207
- RELEASE_REASON
2208
- );
2209
- var HOLD_REASON = {
2210
- PAYMENT_VERIFICATION: "payment_verification",
2211
- FRAUD_CHECK: "fraud_check",
2212
- MANUAL_REVIEW: "manual_review",
2213
- DISPUTE: "dispute",
2214
- COMPLIANCE: "compliance"
2215
- };
2216
- var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
2217
- new Set(HOLD_STATUS_VALUES);
2218
- new Set(RELEASE_REASON_VALUES);
2219
- new Set(HOLD_REASON_VALUES);
2220
-
2221
- // src/enums/split.enums.ts
2222
- var SPLIT_TYPE = {
2223
- PLATFORM_COMMISSION: "platform_commission",
2224
- AFFILIATE_COMMISSION: "affiliate_commission",
2225
- REFERRAL_COMMISSION: "referral_commission",
2226
- PARTNER_COMMISSION: "partner_commission",
2227
- CUSTOM: "custom"
2228
- };
2229
- var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
2230
- var SPLIT_STATUS = {
2231
- PENDING: "pending",
2232
- DUE: "due",
2233
- PAID: "paid",
2234
- WAIVED: "waived",
2235
- CANCELLED: "cancelled"
2236
- };
2237
- var SPLIT_STATUS_VALUES = Object.values(
2238
- SPLIT_STATUS
2239
- );
2240
- var PAYOUT_METHOD = {
2241
- BANK_TRANSFER: "bank_transfer",
2242
- MOBILE_WALLET: "mobile_wallet",
2243
- PLATFORM_BALANCE: "platform_balance",
2244
- CRYPTO: "crypto",
2245
- CHECK: "check",
2246
- MANUAL: "manual"
2247
- };
2248
- var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
2249
- new Set(SPLIT_TYPE_VALUES);
2250
- new Set(SPLIT_STATUS_VALUES);
2251
- new Set(PAYOUT_METHOD_VALUES);
2252
-
2253
- // src/utils/commission-split.ts
2254
- function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2255
- if (!splitRules || splitRules.length === 0) {
2256
- return [];
2257
- }
2258
- if (amount < 0) {
2259
- throw new Error("Transaction amount cannot be negative");
2260
- }
2261
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
2262
- throw new Error("Gateway fee rate must be between 0 and 1");
2263
- }
2264
- const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
2265
- if (totalRate > 1) {
2266
- throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
2267
- }
2268
- return splitRules.map((rule, index) => {
2269
- if (rule.rate < 0 || rule.rate > 1) {
2270
- throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
2271
- }
2272
- const grossAmount = Math.round(amount * rule.rate * 100) / 100;
2273
- const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
2274
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
2275
- return {
2276
- type: rule.type ?? SPLIT_TYPE.CUSTOM,
2277
- recipientId: rule.recipientId,
2278
- recipientType: rule.recipientType,
2279
- rate: rule.rate,
2280
- grossAmount,
2281
- gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
2282
- gatewayFeeAmount,
2283
- netAmount,
2284
- status: SPLIT_STATUS.PENDING,
2285
- dueDate: rule.dueDate ?? null,
2286
- metadata: rule.metadata ?? {}
2287
- };
2288
- });
2289
- }
2290
- function calculateOrganizationPayout(amount, splits = []) {
2291
- const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
2292
- return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
2293
- }
2294
-
2295
- // src/services/escrow.service.ts
2296
- var EscrowService = class {
2297
- models;
2298
- hooks;
2299
- logger;
2300
- constructor(container) {
2301
- this.models = container.get("models");
2302
- this.hooks = container.get("hooks");
2303
- this.logger = container.get("logger");
2304
- }
2305
- /**
2306
- * Hold funds in escrow
2307
- *
2308
- * @param transactionId - Transaction to hold
2309
- * @param options - Hold options
2310
- * @returns Updated transaction
2311
- */
2312
- async hold(transactionId, options = {}) {
2313
- const {
2314
- reason = HOLD_REASON.PAYMENT_VERIFICATION,
2315
- holdUntil = null,
2316
- metadata = {}
2317
- } = options;
2318
- const TransactionModel = this.models.Transaction;
2319
- const transaction = await TransactionModel.findById(transactionId);
2320
- if (!transaction) {
2321
- throw new TransactionNotFoundError(transactionId);
2322
- }
2323
- if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
2324
- throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
2325
- }
2326
- transaction.hold = {
2327
- status: HOLD_STATUS.HELD,
2328
- heldAmount: transaction.amount,
2329
- releasedAmount: 0,
2330
- reason,
2331
- heldAt: /* @__PURE__ */ new Date(),
2332
- ...holdUntil && { holdUntil },
2333
- releases: [],
2334
- metadata
2335
- };
2336
- await transaction.save();
2337
- this._triggerHook("escrow.held", {
2338
- transaction,
2339
- heldAmount: transaction.amount,
2340
- reason
2341
- });
2342
- return transaction;
2343
- }
2344
- /**
2345
- * Release funds from escrow to recipient
2346
- *
2347
- * @param transactionId - Transaction to release
2348
- * @param options - Release options
2349
- * @returns { transaction, releaseTransaction }
2350
- */
2351
- async release(transactionId, options) {
2352
- const {
2353
- amount = null,
2354
- recipientId,
2355
- recipientType = "organization",
2356
- reason = RELEASE_REASON.PAYMENT_VERIFIED,
2357
- releasedBy = null,
2358
- createTransaction = true,
2359
- metadata = {}
2360
- } = options;
2361
- const TransactionModel = this.models.Transaction;
2362
- const transaction = await TransactionModel.findById(transactionId);
2363
- if (!transaction) {
2364
- throw new TransactionNotFoundError(transactionId);
2365
- }
2366
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2367
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2368
- }
2369
- if (!recipientId) {
2370
- throw new Error("recipientId is required for release");
2371
- }
2372
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
2373
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
2374
- if (releaseAmount > availableAmount) {
2375
- throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
2376
- }
2377
- const releaseRecord = {
2378
- amount: releaseAmount,
2379
- recipientId,
2380
- recipientType,
2381
- releasedAt: /* @__PURE__ */ new Date(),
2382
- releasedBy,
2383
- reason,
2384
- metadata
2385
- };
2386
- transaction.hold.releases.push(releaseRecord);
2387
- transaction.hold.releasedAmount += releaseAmount;
2388
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
2389
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
2390
- if (isFullRelease) {
2391
- transaction.hold.status = HOLD_STATUS.RELEASED;
2392
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
2393
- transaction.status = TRANSACTION_STATUS.COMPLETED;
2394
- } else if (isPartialRelease) {
2395
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
2396
- }
2397
- await transaction.save();
2398
- let releaseTransaction = null;
2399
- if (createTransaction) {
2400
- releaseTransaction = await TransactionModel.create({
2401
- organizationId: transaction.organizationId,
2402
- customerId: recipientId,
2403
- amount: releaseAmount,
2404
- currency: transaction.currency,
2405
- category: transaction.category,
2406
- type: TRANSACTION_TYPE.INCOME,
2407
- method: transaction.method,
2408
- status: TRANSACTION_STATUS.COMPLETED,
2409
- gateway: transaction.gateway,
2410
- referenceId: transaction.referenceId,
2411
- referenceModel: transaction.referenceModel,
2412
- metadata: {
2413
- ...metadata,
2414
- isRelease: true,
2415
- heldTransactionId: transaction._id.toString(),
2416
- releaseReason: reason,
2417
- recipientType
2418
- },
2419
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
2420
- });
2421
- }
2422
- this._triggerHook("escrow.released", {
2423
- transaction,
2424
- releaseTransaction,
2425
- releaseAmount,
2426
- recipientId,
2427
- recipientType,
2428
- reason,
2429
- isFullRelease,
2430
- isPartialRelease
2431
- });
2432
- return {
2433
- transaction,
2434
- releaseTransaction,
2435
- releaseAmount,
2436
- isFullRelease,
2437
- isPartialRelease
2438
- };
2439
- }
2440
- /**
2441
- * Cancel hold and release back to customer
2442
- *
2443
- * @param transactionId - Transaction to cancel hold
2444
- * @param options - Cancel options
2445
- * @returns Updated transaction
2446
- */
2447
- async cancel(transactionId, options = {}) {
2448
- const { reason = "Hold cancelled", metadata = {} } = options;
2449
- const TransactionModel = this.models.Transaction;
2450
- const transaction = await TransactionModel.findById(transactionId);
2451
- if (!transaction) {
2452
- throw new TransactionNotFoundError(transactionId);
2453
- }
2454
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2455
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2456
- }
2457
- transaction.hold.status = HOLD_STATUS.CANCELLED;
2458
- transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
2459
- transaction.hold.metadata = {
2460
- ...transaction.hold.metadata,
2461
- ...metadata,
2462
- cancelReason: reason
2463
- };
2464
- transaction.status = TRANSACTION_STATUS.CANCELLED;
2465
- await transaction.save();
2466
- this._triggerHook("escrow.cancelled", {
2467
- transaction,
2468
- reason
2469
- });
2470
- return transaction;
2471
- }
2472
- /**
2473
- * Split payment to multiple recipients
2474
- * Deducts splits from held amount and releases remainder to organization
2475
- *
2476
- * @param transactionId - Transaction to split
2477
- * @param splitRules - Split configuration
2478
- * @returns { transaction, splitTransactions, organizationTransaction }
2479
- */
2480
- async split(transactionId, splitRules = []) {
2481
- const TransactionModel = this.models.Transaction;
2482
- const transaction = await TransactionModel.findById(transactionId);
2483
- if (!transaction) {
2484
- throw new TransactionNotFoundError(transactionId);
2485
- }
2486
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2487
- throw new Error(`Transaction must be held before splitting. Current: ${transaction.hold?.status ?? "none"}`);
2488
- }
2489
- if (!splitRules || splitRules.length === 0) {
2490
- throw new Error("splitRules cannot be empty");
2491
- }
2492
- const splits = calculateSplits(
2493
- transaction.amount,
2494
- splitRules,
2495
- transaction.commission?.gatewayFeeRate ?? 0
2496
- );
2497
- transaction.splits = splits;
2498
- await transaction.save();
2499
- const splitTransactions = [];
2500
- for (const split of splits) {
2501
- const splitTransaction = await TransactionModel.create({
2502
- organizationId: transaction.organizationId,
2503
- customerId: split.recipientId,
2504
- amount: split.netAmount,
2505
- currency: transaction.currency,
2506
- category: split.type,
2507
- type: TRANSACTION_TYPE.EXPENSE,
2508
- method: transaction.method,
2509
- status: TRANSACTION_STATUS.COMPLETED,
2510
- gateway: transaction.gateway,
2511
- referenceId: transaction.referenceId,
2512
- referenceModel: transaction.referenceModel,
2513
- metadata: {
2514
- isSplit: true,
2515
- splitType: split.type,
2516
- recipientType: split.recipientType,
2517
- originalTransactionId: transaction._id.toString(),
2518
- grossAmount: split.grossAmount,
2519
- gatewayFeeAmount: split.gatewayFeeAmount
2520
- },
2521
- idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
2522
- });
2523
- split.payoutTransactionId = splitTransaction._id.toString();
2524
- split.status = SPLIT_STATUS.PAID;
2525
- split.paidDate = /* @__PURE__ */ new Date();
2526
- splitTransactions.push(splitTransaction);
2527
- }
2528
- await transaction.save();
2529
- const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
2530
- const organizationTransaction = await this.release(transactionId, {
2531
- amount: organizationPayout,
2532
- recipientId: transaction.organizationId?.toString() ?? "",
2533
- recipientType: "organization",
2534
- reason: RELEASE_REASON.PAYMENT_VERIFIED,
2535
- createTransaction: true,
2536
- metadata: {
2537
- afterSplits: true,
2538
- totalSplits: splits.length,
2539
- totalSplitAmount: transaction.amount - organizationPayout
2540
- }
2541
- });
2542
- this._triggerHook("escrow.split", {
2543
- transaction,
2544
- splits,
2545
- splitTransactions,
2546
- organizationTransaction: organizationTransaction.releaseTransaction,
2547
- organizationPayout
2548
- });
2549
- return {
2550
- transaction,
2551
- splits,
2552
- splitTransactions,
2553
- organizationTransaction: organizationTransaction.releaseTransaction,
2554
- organizationPayout
2555
- };
2556
- }
2557
- /**
2558
- * Get escrow status
2559
- *
2560
- * @param transactionId - Transaction ID
2561
- * @returns Escrow status
2562
- */
2563
- async getStatus(transactionId) {
2564
- const TransactionModel = this.models.Transaction;
2565
- const transaction = await TransactionModel.findById(transactionId);
2566
- if (!transaction) {
2567
- throw new TransactionNotFoundError(transactionId);
2568
- }
2569
- return {
2570
- transaction,
2571
- hold: transaction.hold ?? null,
2572
- splits: transaction.splits ?? [],
2573
- hasHold: !!transaction.hold,
2574
- hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2575
- };
2576
- }
2577
- _triggerHook(event, data) {
2578
- triggerHook(this.hooks, event, data, this.logger);
2579
- }
2580
- };
2581
-
2582
- // src/core/revenue.ts
2583
- var Revenue = class {
2584
- // ============ CORE ============
2585
- _container;
2586
- _events;
2587
- _plugins;
2588
- _idempotency;
2589
- _circuitBreaker;
2590
- _options;
2591
- _logger;
2592
- _providers;
2593
- _config;
2594
- // ============ SERVICES ============
2595
- /** Monetization service - purchases, subscriptions, free items */
2596
- monetization;
2597
- /** Payment service - verify, refund, webhooks */
2598
- payments;
2599
- /** Transaction service - query, update transactions */
2600
- transactions;
2601
- /** Escrow service - hold, release, splits */
2602
- escrow;
2603
- constructor(container, events, plugins, options, providers, config) {
2604
- this._container = container;
2605
- this._events = events;
2606
- this._plugins = plugins;
2607
- this._options = options;
2608
- this._logger = options.logger;
2609
- this._providers = providers;
2610
- this._config = config;
2611
- this._idempotency = createIdempotencyManager({
2612
- ttl: options.idempotencyTtl
2613
- });
2614
- if (options.circuitBreaker) {
2615
- this._circuitBreaker = createCircuitBreaker();
2616
- }
2617
- container.singleton("events", events);
2618
- container.singleton("plugins", plugins);
2619
- container.singleton("idempotency", this._idempotency);
2620
- container.singleton("logger", this._logger);
2621
- this.monetization = new MonetizationService(container);
2622
- this.payments = new PaymentService(container);
2623
- this.transactions = new TransactionService(container);
2624
- this.escrow = new EscrowService(container);
2625
- Object.freeze(this._providers);
2626
- Object.freeze(this._config);
2627
- }
2628
- // ============ STATIC FACTORY ============
2629
- /**
2630
- * Create a new Revenue builder
2631
- *
2632
- * @example
2633
- * ```typescript
2634
- * const revenue = Revenue
2635
- * .create({ defaultCurrency: 'BDT' })
2636
- * .withModels({ Transaction, Subscription })
2637
- * .withProvider('manual', new ManualProvider())
2638
- * .build();
2639
- * ```
2640
- */
2641
- static create(options = {}) {
2642
- return new RevenueBuilder(options);
2643
- }
2644
- // ============ ACCESSORS ============
2645
- /** DI container (for advanced usage) */
2646
- get container() {
2647
- return this._container;
2648
- }
2649
- /** Event bus */
2650
- get events() {
2651
- return this._events;
2652
- }
2653
- /** Registered providers (frozen) */
2654
- get providers() {
2655
- return this._providers;
2656
- }
2657
- /** Configuration (frozen) */
2658
- get config() {
2659
- return this._config;
2660
- }
2661
- /** Default currency */
2662
- get defaultCurrency() {
2663
- return this._options.defaultCurrency;
2664
- }
2665
- /** Current environment */
2666
- get environment() {
2667
- return this._options.environment;
2668
- }
2669
- // ============ PROVIDER METHODS ============
2670
- /**
2671
- * Get a provider by name
2672
- */
2673
- getProvider(name) {
2674
- const provider = this._providers[name];
2675
- if (!provider) {
2676
- throw new ConfigurationError(
2677
- `Provider "${name}" not found. Available: ${Object.keys(this._providers).join(", ")}`
2678
- );
2679
- }
2680
- return provider;
2681
- }
2682
- /**
2683
- * Get all provider names
2684
- */
2685
- getProviderNames() {
2686
- return Object.keys(this._providers);
2687
- }
2688
- /**
2689
- * Check if provider exists
2690
- */
2691
- hasProvider(name) {
2692
- return name in this._providers;
2693
- }
2694
- // ============ EVENT SYSTEM ============
2695
- /**
2696
- * Subscribe to events
2697
- *
2698
- * @example
2699
- * ```typescript
2700
- * revenue.on('payment.succeeded', (event) => {
2701
- * console.log('Payment:', event.transactionId);
2702
- * });
2703
- * ```
2704
- */
2705
- on = (event, handler) => {
2706
- return this._events.on(event, handler);
2707
- };
2708
- /**
2709
- * Subscribe once
2710
- */
2711
- once = (event, handler) => {
2712
- return this._events.once(event, handler);
2713
- };
2714
- /**
2715
- * Unsubscribe
2716
- */
2717
- off = (event, handler) => {
2718
- this._events.off(event, handler);
2719
- };
2720
- /**
2721
- * Emit an event
2722
- */
2723
- emit = (event, payload) => {
2724
- this._events.emit(event, payload);
2725
- };
2726
- // ============ RESILIENCE ============
2727
- /**
2728
- * Execute operation with retry and idempotency
2729
- */
2730
- async execute(operation, options = {}) {
2731
- const { idempotencyKey, params, useRetry = true, useCircuitBreaker = true } = options;
2732
- const idempotentOp = async () => {
2733
- if (idempotencyKey) {
2734
- const result = await this._idempotency.execute(idempotencyKey, params, operation);
2735
- if (!result.ok) throw result.error;
2736
- return result.value;
2737
- }
2738
- return operation();
2739
- };
2740
- const resilientOp = async () => {
2741
- if (useCircuitBreaker && this._circuitBreaker) {
2742
- return this._circuitBreaker.execute(idempotentOp);
2743
- }
2744
- return idempotentOp();
2745
- };
2746
- if (useRetry && this._options.retry) {
2747
- return tryCatch(() => retry(resilientOp, this._options.retry));
2748
- }
2749
- return tryCatch(resilientOp);
2750
- }
2751
- /**
2752
- * Create plugin context (for advanced usage)
2753
- */
2754
- createContext(meta = {}) {
2755
- return {
2756
- events: this._events,
2757
- logger: this._logger,
2758
- get: (key) => this._container.get(key),
2759
- storage: /* @__PURE__ */ new Map(),
2760
- meta: {
2761
- ...meta,
2762
- requestId: nanoid(12),
2763
- timestamp: /* @__PURE__ */ new Date()
2764
- }
2765
- };
2766
- }
2767
- /**
2768
- * Destroy instance and cleanup
2769
- */
2770
- async destroy() {
2771
- await this._plugins.destroy();
2772
- this._events.clear();
2773
- }
2774
- };
2775
- var RevenueBuilder = class {
2776
- options;
2777
- models = null;
2778
- providers = {};
2779
- plugins = [];
2780
- hooks = {};
2781
- categoryMappings = {};
2782
- constructor(options = {}) {
2783
- this.options = options;
2784
- }
2785
- /**
2786
- * Register models (required)
2787
- *
2788
- * @example
2789
- * ```typescript
2790
- * .withModels({
2791
- * Transaction: TransactionModel,
2792
- * Subscription: SubscriptionModel,
2793
- * })
2794
- * ```
2795
- */
2796
- withModels(models) {
2797
- this.models = models;
2798
- return this;
2799
- }
2800
- /**
2801
- * Register a single model
2802
- */
2803
- withModel(name, model) {
2804
- if (!this.models) {
2805
- this.models = { Transaction: model };
2806
- }
2807
- this.models[name] = model;
2808
- return this;
2809
- }
2810
- /**
2811
- * Register a payment provider
2812
- *
2813
- * @example
2814
- * ```typescript
2815
- * .withProvider('stripe', new StripeProvider({ apiKey: '...' }))
2816
- * .withProvider('manual', new ManualProvider())
2817
- * ```
2818
- */
2819
- withProvider(name, provider) {
2820
- this.providers[name] = provider;
2821
- return this;
2822
- }
2823
- /**
2824
- * Register multiple providers at once
2825
- */
2826
- withProviders(providers) {
2827
- this.providers = { ...this.providers, ...providers };
2828
- return this;
2829
- }
2830
- /**
2831
- * Register a plugin
2832
- *
2833
- * @example
2834
- * ```typescript
2835
- * .withPlugin(loggingPlugin())
2836
- * .withPlugin(auditPlugin({ store: saveToDb }))
2837
- * ```
2838
- */
2839
- withPlugin(plugin) {
2840
- this.plugins.push(plugin);
2841
- return this;
2842
- }
2843
- /**
2844
- * Register multiple plugins
2845
- */
2846
- withPlugins(plugins) {
2847
- this.plugins.push(...plugins);
2848
- return this;
2849
- }
2850
- withHooks(hooks) {
2851
- const normalized = {};
2852
- for (const [event, handlerOrHandlers] of Object.entries(hooks)) {
2853
- if (!handlerOrHandlers) continue;
2854
- normalized[event] = Array.isArray(handlerOrHandlers) ? handlerOrHandlers : [handlerOrHandlers];
2855
- }
2856
- this.hooks = { ...this.hooks, ...normalized };
2857
- return this;
2858
- }
2859
- /**
2860
- * Set retry configuration
2861
- *
2862
- * @example
2863
- * ```typescript
2864
- * .withRetry({ maxAttempts: 5, baseDelay: 2000 })
2865
- * ```
2866
- */
2867
- withRetry(config) {
2868
- this.options.retry = config;
2869
- return this;
2870
- }
2871
- /**
2872
- * Enable circuit breaker
2873
- */
2874
- withCircuitBreaker(enabled = true) {
2875
- this.options.circuitBreaker = enabled;
2876
- return this;
2877
- }
2878
- /**
2879
- * Set custom logger
2880
- */
2881
- withLogger(logger) {
2882
- this.options.logger = logger;
2883
- return this;
2884
- }
2885
- /**
2886
- * Set environment
2887
- */
2888
- forEnvironment(env) {
2889
- this.options.environment = env;
2890
- return this;
2891
- }
2892
- /**
2893
- * Enable debug mode
2894
- */
2895
- withDebug(enabled = true) {
2896
- this.options.debug = enabled;
2897
- return this;
2898
- }
2899
- /**
2900
- * Set commission rate (0-100)
2901
- */
2902
- withCommission(rate, gatewayFeeRate = 0) {
2903
- this.options.commissionRate = rate;
2904
- this.options.gatewayFeeRate = gatewayFeeRate;
2905
- return this;
2906
- }
2907
- /**
2908
- * Set category mappings (entity → category)
2909
- *
2910
- * @example
2911
- * ```typescript
2912
- * .withCategoryMappings({
2913
- * PlatformSubscription: 'platform_subscription',
2914
- * CourseEnrollment: 'course_enrollment',
2915
- * ProductOrder: 'product_order',
2916
- * })
2917
- * ```
2918
- */
2919
- withCategoryMappings(mappings) {
2920
- this.categoryMappings = { ...this.categoryMappings, ...mappings };
2921
- return this;
2922
- }
2923
- /**
2924
- * Build the Revenue instance
2925
- */
2926
- build() {
2927
- if (!this.models) {
2928
- throw new ConfigurationError(
2929
- "Models are required. Use .withModels({ Transaction, Subscription })"
2930
- );
2931
- }
2932
- if (!this.models.Transaction) {
2933
- throw new ConfigurationError(
2934
- "Transaction model is required in models configuration"
2935
- );
2936
- }
2937
- if (Object.keys(this.providers).length === 0) {
2938
- throw new ConfigurationError(
2939
- "At least one provider is required. Use .withProvider(name, provider)"
2940
- );
2941
- }
2942
- const container = new Container();
2943
- const defaultLogger = {
2944
- debug: (msg, data) => this.options.debug && console.debug(`[Revenue] ${msg}`, data ?? ""),
2945
- info: (msg, data) => console.info(`[Revenue] ${msg}`, data ?? ""),
2946
- warn: (msg, data) => console.warn(`[Revenue] ${msg}`, data ?? ""),
2947
- error: (msg, data) => console.error(`[Revenue] ${msg}`, data ?? "")
2948
- };
2949
- const resolvedOptions = {
2950
- defaultCurrency: this.options.defaultCurrency ?? "USD",
2951
- environment: this.options.environment ?? "development",
2952
- debug: this.options.debug ?? false,
2953
- retry: this.options.retry ?? { maxAttempts: 3 },
2954
- idempotencyTtl: this.options.idempotencyTtl ?? 24 * 60 * 60 * 1e3,
2955
- circuitBreaker: this.options.circuitBreaker ?? false,
2956
- logger: this.options.logger ?? defaultLogger,
2957
- commissionRate: this.options.commissionRate ?? 0,
2958
- gatewayFeeRate: this.options.gatewayFeeRate ?? 0
2959
- };
2960
- const config = {
2961
- defaultCurrency: resolvedOptions.defaultCurrency,
2962
- commissionRate: resolvedOptions.commissionRate,
2963
- gatewayFeeRate: resolvedOptions.gatewayFeeRate,
2964
- categoryMappings: this.categoryMappings
2965
- };
2966
- container.singleton("models", this.models);
2967
- container.singleton("providers", this.providers);
2968
- container.singleton("hooks", this.hooks);
2969
- container.singleton("config", config);
2970
- const events = createEventBus();
2971
- const pluginManager = new PluginManager();
2972
- for (const plugin of this.plugins) {
2973
- pluginManager.register(plugin);
2974
- }
2975
- const revenue = new Revenue(
2976
- container,
2977
- events,
2978
- pluginManager,
2979
- resolvedOptions,
2980
- this.providers,
2981
- config
2982
- );
2983
- const ctx = revenue.createContext();
2984
- pluginManager.init(ctx).catch((err2) => {
2985
- resolvedOptions.logger.error("Failed to initialize plugins", err2);
2986
- });
2987
- return revenue;
2988
- }
2989
- };
2990
- function createRevenue(config) {
2991
- let builder = Revenue.create(config.options);
2992
- builder = builder.withModels(config.models);
2993
- builder = builder.withProviders(config.providers);
2994
- if (config.plugins) {
2995
- builder = builder.withPlugins(config.plugins);
2996
- }
2997
- if (config.hooks) {
2998
- builder = builder.withHooks(config.hooks);
2999
- }
3000
- return builder.build();
3001
- }
3002
-
3003
- 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 };
3004
- //# sourceMappingURL=index.js.map
3005
- //# sourceMappingURL=index.js.map