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