@classytic/revenue 1.1.3 → 2.0.0

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 (82) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +638 -632
  3. package/dist/audit-B39B0Sdq.mjs +53 -0
  4. package/dist/audit-DZ0eTr9g.d.mts +89 -0
  5. package/dist/bridges/index.d.mts +2 -0
  6. package/dist/bridges/index.mjs +1 -0
  7. package/dist/context-DRqSeTPM.d.mts +35 -0
  8. package/dist/core/state-machines.d.mts +35 -0
  9. package/dist/core/state-machines.mjs +134 -0
  10. package/dist/engine-types-CcjIb4Fy.d.mts +611 -0
  11. package/dist/enums/index.d.mts +3 -157
  12. package/dist/enums/index.mjs +3 -55
  13. package/dist/errors-DHa8JVQ-.mjs +92 -0
  14. package/dist/escrow.schema-BBv9oVEW.mjs +322 -0
  15. package/dist/escrow.schema-CC8XuD46.d.mts +629 -0
  16. package/dist/event-constants-CEMitnIV.mjs +53 -0
  17. package/dist/events/index.d.mts +3 -0
  18. package/dist/events/index.mjs +4 -0
  19. package/dist/index.d.mts +77 -9
  20. package/dist/index.mjs +465 -29
  21. package/dist/monetization.enums-BtiU3t8o.mjs +39 -0
  22. package/dist/monetization.enums-D2xbxXJM.d.mts +34 -0
  23. package/dist/plugins/plugin.interface.d.mts +28 -0
  24. package/dist/plugins/plugin.interface.mjs +26 -0
  25. package/dist/providers/index.d.mts +2 -3
  26. package/dist/providers/index.mjs +2 -2
  27. package/dist/{base-DCoyIUj6.mjs → registry-DhFMsSn5.mjs} +34 -36
  28. package/dist/{base-CsTlVQJe.d.mts → registry-SvIGPAx_.d.mts} +73 -66
  29. package/dist/repositories/create-repositories.d.mts +21 -0
  30. package/dist/repositories/create-repositories.mjs +12 -0
  31. package/dist/revenue-bridges-sdlrR85c.d.mts +145 -0
  32. package/dist/revenue-event-catalog-BX3g7RUi.d.mts +823 -0
  33. package/dist/revenue-event-catalog-LqxPnsU_.mjs +388 -0
  34. package/dist/settlement.repository-Cy3mMWGH.mjs +771 -0
  35. package/dist/shared/index.d.mts +2 -0
  36. package/dist/shared/index.mjs +4 -0
  37. package/dist/split.enums-CQE3ekH1.mjs +172 -0
  38. package/dist/split.enums-Dw4zCrcZ.d.mts +154 -0
  39. package/dist/splits-BAfY-a9P.mjs +123 -0
  40. package/dist/validators/index.d.mts +2 -0
  41. package/dist/validators/index.mjs +3 -0
  42. package/package.json +32 -36
  43. package/dist/application/services/index.d.mts +0 -4
  44. package/dist/application/services/index.mjs +0 -3
  45. package/dist/category-resolver-DV83N8ok.mjs +0 -284
  46. package/dist/commission-split-BzB8cd39.mjs +0 -485
  47. package/dist/core/events.d.mts +0 -294
  48. package/dist/core/events.mjs +0 -100
  49. package/dist/core/index.d.mts +0 -9
  50. package/dist/core/index.mjs +0 -8
  51. package/dist/errors-CorrWz7A.d.mts +0 -787
  52. package/dist/escrow.enums-CZGrrdg7.mjs +0 -101
  53. package/dist/escrow.enums-DwdLuuve.d.mts +0 -78
  54. package/dist/idempotency-DaYcUGY1.mjs +0 -172
  55. package/dist/index-Dsp7H5Wb.d.mts +0 -471
  56. package/dist/infrastructure/plugins/index.d.mts +0 -239
  57. package/dist/infrastructure/plugins/index.mjs +0 -345
  58. package/dist/money-CvrDOijQ.mjs +0 -271
  59. package/dist/money-DPG8AtJ8.d.mts +0 -112
  60. package/dist/payment.enums-HAuAS9Pp.d.mts +0 -70
  61. package/dist/payment.enums-tEFVa-Xp.mjs +0 -69
  62. package/dist/plugin-BbK0OVHy.d.mts +0 -327
  63. package/dist/plugin-Cd_V04Em.mjs +0 -210
  64. package/dist/reconciliation/index.d.mts +0 -193
  65. package/dist/reconciliation/index.mjs +0 -192
  66. package/dist/retry-HHCOXYdn.d.mts +0 -186
  67. package/dist/revenue-9scqKSef.mjs +0 -553
  68. package/dist/schemas/index.d.mts +0 -2665
  69. package/dist/schemas/index.mjs +0 -717
  70. package/dist/schemas/validation.d.mts +0 -375
  71. package/dist/schemas/validation.mjs +0 -325
  72. package/dist/settlement.enums-DFhkqZEY.d.mts +0 -132
  73. package/dist/settlement.schema-D5uWB5tP.d.mts +0 -344
  74. package/dist/settlement.service-BxuiHpNC.d.mts +0 -594
  75. package/dist/settlement.service-CUxbUTzT.mjs +0 -2510
  76. package/dist/split.enums-BrjabxIX.mjs +0 -86
  77. package/dist/split.enums-DmskfLOM.d.mts +0 -43
  78. package/dist/tax-BoCt5cEd.d.mts +0 -61
  79. package/dist/tax-EQ15DO81.mjs +0 -162
  80. package/dist/transaction.enums-pCyMFT4Z.mjs +0 -96
  81. package/dist/utils/index.d.mts +0 -428
  82. package/dist/utils/index.mjs +0 -346
@@ -1,2510 +0,0 @@
1
- import { a as reverseCommission, c as retry, i as calculateCommission, n as calculateSplits, t as calculateOrganizationPayout } from "./commission-split-BzB8cd39.mjs";
2
- import { C as ValidationError, S as TransactionNotFoundError, _ as RefundNotSupportedError, a as InvalidAmountError, b as SubscriptionNotActiveError, c as ModelNotRegisteredError, d as PaymentIntentCreationError, f as PaymentVerificationError, g as RefundError, h as ProviderNotFoundError, m as ProviderError, n as AlreadyVerifiedError, o as InvalidStateTransitionError, p as ProviderCapabilityError, s as MissingRequiredFieldError, t as resolveCategory, x as SubscriptionNotFoundError } from "./category-resolver-DV83N8ok.mjs";
3
- import { a as TRANSACTION_STATUS, r as TRANSACTION_FLOW } from "./transaction.enums-pCyMFT4Z.mjs";
4
- import { a as RELEASE_REASON, f as SETTLEMENT_TYPE, g as MONETIZATION_TYPES, r as HOLD_STATUS, t as HOLD_REASON, u as SETTLEMENT_STATUS } from "./escrow.enums-CZGrrdg7.mjs";
5
- import { f as SUBSCRIPTION_STATUS, r as SPLIT_STATUS } from "./split.enums-BrjabxIX.mjs";
6
- import { nanoid } from "nanoid";
7
-
8
- //#region src/infrastructure/config/resolver.ts
9
- /**
10
- * Resolve builder options to service config
11
- *
12
- * Converts singular commission rates to category/gateway maps
13
- * Uses '*' as global default key (applies to all categories/gateways)
14
- *
15
- * @param options - Builder options
16
- * @returns Service-ready config
17
- *
18
- * @example
19
- * ```typescript
20
- * const options = {
21
- * commissionRate: 0.10, // 10% global default
22
- * gatewayFeeRate: 0.029, // 2.9% global default
23
- * };
24
- *
25
- * const config = resolveConfig(options);
26
- * // {
27
- * // commissionRates: { '*': 0.10 },
28
- * // gatewayFeeRates: { '*': 0.029 }
29
- * // }
30
- *
31
- * // Services can then:
32
- * const rate = config.commissionRates?.['course_purchase']
33
- * ?? config.commissionRates?.['*'] // Fallback to global default
34
- * ?? 0;
35
- * ```
36
- */
37
- function resolveConfig(options) {
38
- const config = {
39
- targetModels: [],
40
- categoryMappings: {}
41
- };
42
- if (options.commissionRate !== void 0) config.commissionRates = { "*": options.commissionRate };
43
- if (options.gatewayFeeRate !== void 0) config.gatewayFeeRates = { "*": options.gatewayFeeRate };
44
- return config;
45
- }
46
- /**
47
- * Get commission rate for a category
48
- *
49
- * Follows precedence: specific category → global default → 0
50
- *
51
- * @param config - Revenue config
52
- * @param category - Transaction category
53
- * @returns Commission rate (0-1)
54
- *
55
- * @example
56
- * ```typescript
57
- * const rate = getCommissionRate(config, 'course_purchase');
58
- * // Checks:
59
- * // 1. config.commissionRates?.['course_purchase']
60
- * // 2. config.commissionRates?.['*']
61
- * // 3. 0 (fallback)
62
- * ```
63
- */
64
- function getCommissionRate(config, category) {
65
- if (!config?.commissionRates) return 0;
66
- if (category in config.commissionRates) return config.commissionRates[category];
67
- return config.commissionRates["*"] ?? 0;
68
- }
69
- /**
70
- * Get gateway fee rate for a gateway
71
- *
72
- * Follows precedence: specific gateway → global default → 0
73
- *
74
- * @param config - Revenue config
75
- * @param gateway - Gateway name (e.g., 'stripe', 'paypal')
76
- * @returns Gateway fee rate (0-1)
77
- */
78
- function getGatewayFeeRate(config, gateway) {
79
- if (!config?.gatewayFeeRates) return 0;
80
- if (gateway in config.gatewayFeeRates) return config.gatewayFeeRates[gateway];
81
- return config.gatewayFeeRates["*"] ?? 0;
82
- }
83
-
84
- //#endregion
85
- //#region src/core/state-machine/StateMachine.ts
86
- /**
87
- * Generic State Machine Validator
88
- * @classytic/revenue
89
- *
90
- * Inspired by Stripe's state transition validation
91
- * Provides centralized, type-safe state transition management
92
- */
93
- /**
94
- * Generic State Machine for validating state transitions
95
- *
96
- * @template TState - The state type (typically a union of string literals)
97
- *
98
- * @example
99
- * ```typescript
100
- * const stateMachine = new StateMachine(
101
- * new Map([
102
- * ['pending', new Set(['processing', 'failed'])],
103
- * ['processing', new Set(['completed', 'failed'])],
104
- * ['completed', new Set([])], // Terminal state
105
- * ['failed', new Set([])], // Terminal state
106
- * ]),
107
- * 'payment'
108
- * );
109
- *
110
- * // Validate transition
111
- * stateMachine.validate('pending', 'processing', 'pay_123'); // ✅ OK
112
- * stateMachine.validate('completed', 'pending', 'pay_123'); // ❌ Throws InvalidStateTransitionError
113
- *
114
- * // Check without throwing
115
- * stateMachine.canTransition('pending', 'processing'); // true
116
- * stateMachine.canTransition('completed', 'pending'); // false
117
- * ```
118
- */
119
- var StateMachine = class {
120
- /**
121
- * @param transitions - Map of state → allowed next states
122
- * @param resourceType - Type of resource (for error messages)
123
- */
124
- constructor(transitions, resourceType) {
125
- this.transitions = transitions;
126
- this.resourceType = resourceType;
127
- }
128
- /**
129
- * Validate state transition is allowed
130
- *
131
- * @param from - Current state
132
- * @param to - Target state
133
- * @param resourceId - ID of the resource being transitioned
134
- * @throws InvalidStateTransitionError if transition is invalid
135
- *
136
- * @example
137
- * ```typescript
138
- * try {
139
- * stateMachine.validate('pending', 'completed', 'tx_123');
140
- * } catch (error) {
141
- * if (error instanceof InvalidStateTransitionError) {
142
- * console.error('Invalid transition:', error.message);
143
- * }
144
- * }
145
- * ```
146
- */
147
- validate(from, to, resourceId) {
148
- if (!this.transitions.get(from)?.has(to)) throw new InvalidStateTransitionError(this.resourceType, resourceId, from, to);
149
- }
150
- /**
151
- * Check if transition is valid (non-throwing)
152
- *
153
- * @param from - Current state
154
- * @param to - Target state
155
- * @returns true if transition is allowed
156
- *
157
- * @example
158
- * ```typescript
159
- * if (stateMachine.canTransition('pending', 'processing')) {
160
- * // Safe to proceed with transition
161
- * transaction.status = 'processing';
162
- * }
163
- * ```
164
- */
165
- canTransition(from, to) {
166
- return this.transitions.get(from)?.has(to) ?? false;
167
- }
168
- /**
169
- * Get all allowed next states from current state
170
- *
171
- * @param from - Current state
172
- * @returns Array of allowed next states
173
- *
174
- * @example
175
- * ```typescript
176
- * const nextStates = stateMachine.getAllowedTransitions('pending');
177
- * console.log(nextStates); // ['processing', 'failed']
178
- * ```
179
- */
180
- getAllowedTransitions(from) {
181
- return Array.from(this.transitions.get(from) ?? []);
182
- }
183
- /**
184
- * Check if state is terminal (no outgoing transitions)
185
- *
186
- * @param state - State to check
187
- * @returns true if state has no outgoing transitions
188
- *
189
- * @example
190
- * ```typescript
191
- * stateMachine.isTerminalState('completed'); // true
192
- * stateMachine.isTerminalState('pending'); // false
193
- * ```
194
- */
195
- isTerminalState(state) {
196
- const transitions = this.transitions.get(state);
197
- return !transitions || transitions.size === 0;
198
- }
199
- /**
200
- * Get the resource type this state machine manages
201
- *
202
- * @returns Resource type string
203
- */
204
- getResourceType() {
205
- return this.resourceType;
206
- }
207
- /**
208
- * Validate state transition and create audit event
209
- *
210
- * This is a convenience method that combines validation with audit event creation.
211
- * Use this when you want to both validate a transition and record it in the audit trail.
212
- *
213
- * @param from - Current state
214
- * @param to - Target state
215
- * @param resourceId - ID of the resource being transitioned
216
- * @param context - Optional audit context (who, why, metadata)
217
- * @returns StateChangeEvent ready to be appended to document metadata
218
- * @throws InvalidStateTransitionError if transition is invalid
219
- *
220
- * @example
221
- * ```typescript
222
- * import { appendAuditEvent } from '@classytic/revenue';
223
- *
224
- * // Validate and create audit event
225
- * const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
226
- * transaction.status,
227
- * 'verified',
228
- * transaction._id.toString(),
229
- * {
230
- * changedBy: 'admin_123',
231
- * reason: 'Payment verified by payment gateway',
232
- * metadata: { verificationId: 'ver_abc' }
233
- * }
234
- * );
235
- *
236
- * // Apply state change
237
- * transaction.status = 'verified';
238
- *
239
- * // Append audit event to metadata
240
- * Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
241
- *
242
- * // Save
243
- * await transaction.save();
244
- * ```
245
- */
246
- validateAndCreateAuditEvent(from, to, resourceId, context) {
247
- this.validate(from, to, resourceId);
248
- return {
249
- resourceType: this.resourceType,
250
- resourceId,
251
- fromState: from,
252
- toState: to,
253
- changedAt: /* @__PURE__ */ new Date(),
254
- changedBy: context?.changedBy,
255
- reason: context?.reason,
256
- metadata: context?.metadata
257
- };
258
- }
259
- };
260
-
261
- //#endregion
262
- //#region src/core/state-machine/definitions.ts
263
- /**
264
- * State Machine Definitions
265
- * @classytic/revenue
266
- *
267
- * Centralized state transition rules for all entities
268
- * Inspired by Stripe, PayPal, and Shopify state management patterns
269
- */
270
- /**
271
- * Transaction State Machine
272
- *
273
- * Flow:
274
- * ```
275
- * pending → payment_initiated → processing → verified → completed
276
- * ↓ ↓ ↓
277
- * failed requires_action refunded
278
- * ↓ partially_refunded
279
- * failed
280
- * ```
281
- *
282
- * Terminal states: failed, refunded, cancelled, expired
283
- */
284
- const TRANSACTION_STATE_MACHINE = new StateMachine(new Map([
285
- [TRANSACTION_STATUS.PENDING, new Set([
286
- TRANSACTION_STATUS.PAYMENT_INITIATED,
287
- TRANSACTION_STATUS.PROCESSING,
288
- TRANSACTION_STATUS.VERIFIED,
289
- TRANSACTION_STATUS.FAILED,
290
- TRANSACTION_STATUS.CANCELLED
291
- ])],
292
- [TRANSACTION_STATUS.PAYMENT_INITIATED, new Set([
293
- TRANSACTION_STATUS.PROCESSING,
294
- TRANSACTION_STATUS.VERIFIED,
295
- TRANSACTION_STATUS.REQUIRES_ACTION,
296
- TRANSACTION_STATUS.FAILED,
297
- TRANSACTION_STATUS.CANCELLED
298
- ])],
299
- [TRANSACTION_STATUS.PROCESSING, new Set([
300
- TRANSACTION_STATUS.VERIFIED,
301
- TRANSACTION_STATUS.REQUIRES_ACTION,
302
- TRANSACTION_STATUS.FAILED
303
- ])],
304
- [TRANSACTION_STATUS.REQUIRES_ACTION, new Set([
305
- TRANSACTION_STATUS.VERIFIED,
306
- TRANSACTION_STATUS.FAILED,
307
- TRANSACTION_STATUS.CANCELLED,
308
- TRANSACTION_STATUS.EXPIRED
309
- ])],
310
- [TRANSACTION_STATUS.VERIFIED, new Set([
311
- TRANSACTION_STATUS.COMPLETED,
312
- TRANSACTION_STATUS.REFUNDED,
313
- TRANSACTION_STATUS.PARTIALLY_REFUNDED,
314
- TRANSACTION_STATUS.CANCELLED
315
- ])],
316
- [TRANSACTION_STATUS.COMPLETED, new Set([TRANSACTION_STATUS.REFUNDED, TRANSACTION_STATUS.PARTIALLY_REFUNDED])],
317
- [TRANSACTION_STATUS.PARTIALLY_REFUNDED, new Set([TRANSACTION_STATUS.REFUNDED])],
318
- [TRANSACTION_STATUS.FAILED, /* @__PURE__ */ new Set([])],
319
- [TRANSACTION_STATUS.REFUNDED, /* @__PURE__ */ new Set([])],
320
- [TRANSACTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
321
- [TRANSACTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
322
- ]), "transaction");
323
- /**
324
- * Subscription State Machine
325
- *
326
- * Flow:
327
- * ```
328
- * pending → active → paused → active
329
- * ↓ ↓
330
- * cancelled cancelled
331
- * ↓
332
- * expired
333
- * ```
334
- *
335
- * Terminal states: cancelled, expired
336
- */
337
- const SUBSCRIPTION_STATE_MACHINE = new StateMachine(new Map([
338
- [SUBSCRIPTION_STATUS.PENDING, new Set([SUBSCRIPTION_STATUS.ACTIVE, SUBSCRIPTION_STATUS.CANCELLED])],
339
- [SUBSCRIPTION_STATUS.ACTIVE, new Set([
340
- SUBSCRIPTION_STATUS.PAUSED,
341
- SUBSCRIPTION_STATUS.PENDING_RENEWAL,
342
- SUBSCRIPTION_STATUS.CANCELLED,
343
- SUBSCRIPTION_STATUS.EXPIRED
344
- ])],
345
- [SUBSCRIPTION_STATUS.PENDING_RENEWAL, new Set([
346
- SUBSCRIPTION_STATUS.ACTIVE,
347
- SUBSCRIPTION_STATUS.CANCELLED,
348
- SUBSCRIPTION_STATUS.EXPIRED
349
- ])],
350
- [SUBSCRIPTION_STATUS.PAUSED, new Set([SUBSCRIPTION_STATUS.ACTIVE, SUBSCRIPTION_STATUS.CANCELLED])],
351
- [SUBSCRIPTION_STATUS.INACTIVE, new Set([SUBSCRIPTION_STATUS.ACTIVE, SUBSCRIPTION_STATUS.CANCELLED])],
352
- [SUBSCRIPTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
353
- [SUBSCRIPTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
354
- ]), "subscription");
355
- /**
356
- * Settlement State Machine
357
- *
358
- * Flow:
359
- * ```
360
- * pending → processing → completed
361
- * ↓
362
- * failed → pending (retry allowed)
363
- * ```
364
- *
365
- * Terminal states: completed, cancelled
366
- * Note: failed can retry to pending
367
- */
368
- const SETTLEMENT_STATE_MACHINE = new StateMachine(new Map([
369
- [SETTLEMENT_STATUS.PENDING, new Set([SETTLEMENT_STATUS.PROCESSING, SETTLEMENT_STATUS.CANCELLED])],
370
- [SETTLEMENT_STATUS.PROCESSING, new Set([SETTLEMENT_STATUS.COMPLETED, SETTLEMENT_STATUS.FAILED])],
371
- [SETTLEMENT_STATUS.FAILED, new Set([SETTLEMENT_STATUS.PENDING, SETTLEMENT_STATUS.CANCELLED])],
372
- [SETTLEMENT_STATUS.COMPLETED, /* @__PURE__ */ new Set([])],
373
- [SETTLEMENT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
374
- ]), "settlement");
375
- /**
376
- * Escrow Hold State Machine
377
- *
378
- * Flow:
379
- * ```
380
- * held → partially_released → released
381
- * ↓
382
- * cancelled
383
- * ```
384
- *
385
- * Terminal states: released, cancelled, expired
386
- */
387
- const HOLD_STATE_MACHINE = new StateMachine(new Map([
388
- [HOLD_STATUS.HELD, new Set([
389
- HOLD_STATUS.RELEASED,
390
- HOLD_STATUS.PARTIALLY_RELEASED,
391
- HOLD_STATUS.CANCELLED,
392
- HOLD_STATUS.EXPIRED
393
- ])],
394
- [HOLD_STATUS.PARTIALLY_RELEASED, new Set([HOLD_STATUS.RELEASED, HOLD_STATUS.CANCELLED])],
395
- [HOLD_STATUS.RELEASED, /* @__PURE__ */ new Set([])],
396
- [HOLD_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
397
- [HOLD_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
398
- ]), "escrow_hold");
399
- /**
400
- * Split Payment State Machine
401
- *
402
- * Flow:
403
- * ```
404
- * pending → due → paid
405
- * ↓
406
- * waived
407
- * cancelled
408
- * ```
409
- *
410
- * Terminal states: paid, waived, cancelled
411
- */
412
- const SPLIT_STATE_MACHINE = new StateMachine(new Map([
413
- [SPLIT_STATUS.PENDING, new Set([
414
- SPLIT_STATUS.DUE,
415
- SPLIT_STATUS.PAID,
416
- SPLIT_STATUS.WAIVED,
417
- SPLIT_STATUS.CANCELLED
418
- ])],
419
- [SPLIT_STATUS.DUE, new Set([
420
- SPLIT_STATUS.PAID,
421
- SPLIT_STATUS.WAIVED,
422
- SPLIT_STATUS.CANCELLED
423
- ])],
424
- [SPLIT_STATUS.PAID, /* @__PURE__ */ new Set([])],
425
- [SPLIT_STATUS.WAIVED, /* @__PURE__ */ new Set([])],
426
- [SPLIT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
427
- ]), "split");
428
-
429
- //#endregion
430
- //#region src/infrastructure/audit/types.ts
431
- /**
432
- * Helper to append audit event to document metadata
433
- *
434
- * This is a metadata-based approach that stores audit trail
435
- * directly in the document's metadata.stateHistory array.
436
- *
437
- * Example:
438
- * ```typescript
439
- * const transaction = await Transaction.findById(id);
440
- *
441
- * const auditEvent = {
442
- * resourceType: 'transaction',
443
- * resourceId: transaction._id.toString(),
444
- * fromState: 'pending',
445
- * toState: 'verified',
446
- * changedAt: new Date(),
447
- * changedBy: 'admin_123',
448
- * };
449
- *
450
- * const updated = appendAuditEvent(transaction, auditEvent);
451
- * await updated.save();
452
- * ```
453
- *
454
- * @param document - Mongoose document or plain object with optional metadata field
455
- * @param event - State change event to append
456
- * @returns Updated document with event in metadata.stateHistory
457
- */
458
- function appendAuditEvent(document, event) {
459
- const stateHistory = document.metadata?.stateHistory ?? [];
460
- return {
461
- ...document,
462
- metadata: {
463
- ...document.metadata,
464
- stateHistory: [...stateHistory, event]
465
- }
466
- };
467
- }
468
- /**
469
- * Helper to get audit trail from document metadata
470
- *
471
- * Example:
472
- * ```typescript
473
- * const transaction = await Transaction.findById(id);
474
- * const history = getAuditTrail(transaction);
475
- *
476
- * console.log(history);
477
- * // [
478
- * // { fromState: 'pending', toState: 'processing', changedAt: ... },
479
- * // { fromState: 'processing', toState: 'verified', changedAt: ... },
480
- * // ]
481
- * ```
482
- *
483
- * @param document - Document to extract audit trail from
484
- * @returns Array of state change events (empty array if none)
485
- */
486
- function getAuditTrail(document) {
487
- return document.metadata?.stateHistory ?? [];
488
- }
489
- /**
490
- * Helper to get the last state change from audit trail
491
- *
492
- * @param document - Document to extract last change from
493
- * @returns Last state change event or undefined if none
494
- */
495
- function getLastStateChange(document) {
496
- const history = getAuditTrail(document);
497
- return history[history.length - 1];
498
- }
499
- /**
500
- * Helper to find state changes by criteria
501
- *
502
- * Example:
503
- * ```typescript
504
- * const transaction = await Transaction.findById(id);
505
- *
506
- * // Find all changes made by a specific user
507
- * const userChanges = filterAuditTrail(transaction, {
508
- * changedBy: 'admin_123'
509
- * });
510
- *
511
- * // Find all transitions to 'failed' state
512
- * const failures = filterAuditTrail(transaction, {
513
- * toState: 'failed'
514
- * });
515
- * ```
516
- *
517
- * @param document - Document to filter audit trail from
518
- * @param criteria - Filter criteria
519
- * @returns Filtered array of state change events
520
- */
521
- function filterAuditTrail(document, criteria) {
522
- return getAuditTrail(document).filter((event) => {
523
- return Object.entries(criteria).every(([key, value]) => {
524
- return event[key] === value;
525
- });
526
- });
527
- }
528
-
529
- //#endregion
530
- //#region src/application/services/monetization.service.ts
531
- /**
532
- * Monetization Service
533
- * @classytic/revenue
534
- *
535
- * Framework-agnostic monetization management service with DI
536
- * Handles purchases, subscriptions, and free items using provider system
537
- */
538
- /**
539
- * Monetization Service
540
- * Uses DI container for all dependencies
541
- *
542
- * Architecture:
543
- * - PluginManager: Wraps operations with lifecycle hooks (before/after)
544
- * - EventBus: Fire-and-forget notifications for completed operations
545
- */
546
- var MonetizationService = class {
547
- models;
548
- providers;
549
- config;
550
- plugins;
551
- logger;
552
- events;
553
- retryConfig;
554
- circuitBreaker;
555
- constructor(container) {
556
- this.models = container.get("models");
557
- this.providers = container.get("providers");
558
- this.config = container.get("config");
559
- this.plugins = container.get("plugins");
560
- this.logger = container.get("logger");
561
- this.events = container.get("events");
562
- this.retryConfig = container.get("retryConfig");
563
- this.circuitBreaker = container.get("circuitBreaker");
564
- }
565
- /**
566
- * Create plugin context for hook execution
567
- * @private
568
- */
569
- getPluginContext(idempotencyKey) {
570
- return {
571
- events: this.events,
572
- logger: this.logger,
573
- storage: /* @__PURE__ */ new Map(),
574
- meta: {
575
- idempotencyKey: idempotencyKey ?? void 0,
576
- requestId: nanoid(),
577
- timestamp: /* @__PURE__ */ new Date()
578
- }
579
- };
580
- }
581
- /**
582
- * Execute provider call with retry and circuit breaker protection
583
- * @private
584
- */
585
- async executeProviderCall(operation, operationName) {
586
- const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
587
- if (this.retryConfig && Object.keys(this.retryConfig).length > 0) return retry(withCircuitBreaker, {
588
- ...this.retryConfig,
589
- onRetry: (error, attempt, delay) => {
590
- this.logger.warn(`[${operationName}] Retry attempt ${attempt} after ${delay}ms:`, error);
591
- this.retryConfig.onRetry?.(error, attempt, delay);
592
- }
593
- });
594
- return withCircuitBreaker();
595
- }
596
- /**
597
- * Create a new monetization (purchase, subscription, or free item)
598
- *
599
- * @param params - Monetization parameters
600
- *
601
- * @example
602
- * // One-time purchase
603
- * await revenue.monetization.create({
604
- * data: {
605
- * organizationId: '...',
606
- * customerId: '...',
607
- * sourceId: order._id,
608
- * sourceModel: 'Order',
609
- * },
610
- * planKey: 'one_time',
611
- * monetizationType: 'purchase',
612
- * gateway: 'bkash',
613
- * amount: 1500,
614
- * });
615
- *
616
- * // Recurring subscription
617
- * await revenue.monetization.create({
618
- * data: {
619
- * organizationId: '...',
620
- * customerId: '...',
621
- * sourceId: subscription._id,
622
- * sourceModel: 'Subscription',
623
- * },
624
- * planKey: 'monthly',
625
- * monetizationType: 'subscription',
626
- * gateway: 'stripe',
627
- * amount: 2000,
628
- * });
629
- *
630
- * @returns Result with subscription, transaction, and paymentIntent
631
- */
632
- async create(params) {
633
- return this.plugins.executeHook("monetization.create.before", this.getPluginContext(params.idempotencyKey), params, async () => {
634
- const { data, planKey, amount, currency = "BDT", gateway = "manual", entity = null, monetizationType = MONETIZATION_TYPES.SUBSCRIPTION, paymentData, metadata = {}, idempotencyKey = null } = params;
635
- if (!planKey) throw new MissingRequiredFieldError("planKey");
636
- if (amount < 0) throw new InvalidAmountError(amount);
637
- const isFree = amount === 0;
638
- const provider = this.providers[gateway];
639
- if (!provider) throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
640
- let paymentIntent = null;
641
- let transaction = null;
642
- if (!isFree) {
643
- try {
644
- paymentIntent = await this.executeProviderCall(() => provider.createIntent({
645
- amount,
646
- currency,
647
- metadata: {
648
- ...metadata,
649
- type: "subscription",
650
- planKey
651
- }
652
- }), `${gateway}.createIntent`);
653
- } catch (error) {
654
- throw new PaymentIntentCreationError(gateway, error);
655
- }
656
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
657
- const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_FLOW.INFLOW;
658
- const commission = calculateCommission(amount, getCommissionRate(this.config, category), getGatewayFeeRate(this.config, gateway));
659
- const tax = params.tax;
660
- const TransactionModel = this.models.Transaction;
661
- const baseAmount = tax?.pricesIncludeTax ? tax.baseAmount : amount;
662
- const feeAmount = commission?.gatewayFeeAmount || 0;
663
- const taxAmount = tax?.taxAmount || 0;
664
- const netAmount = baseAmount - feeAmount - taxAmount;
665
- transaction = await TransactionModel.create({
666
- organizationId: data.organizationId,
667
- customerId: data.customerId ?? null,
668
- type: category,
669
- flow: transactionFlow,
670
- tags: category === "subscription" ? ["recurring", "subscription"] : [],
671
- amount: baseAmount,
672
- currency,
673
- fee: feeAmount,
674
- tax: taxAmount,
675
- net: netAmount,
676
- ...tax && { taxDetails: {
677
- type: tax.type === "collected" ? "sales_tax" : tax.type === "paid" ? "vat" : "none",
678
- rate: tax.rate || 0,
679
- isInclusive: tax.pricesIncludeTax || false
680
- } },
681
- method: paymentData?.method ?? "manual",
682
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
683
- gateway: {
684
- type: gateway,
685
- provider: gateway,
686
- sessionId: paymentIntent.sessionId,
687
- paymentIntentId: paymentIntent.paymentIntentId,
688
- chargeId: paymentIntent.id,
689
- metadata: paymentIntent.metadata
690
- },
691
- paymentDetails: { ...paymentData },
692
- ...commission && { commission },
693
- ...data.sourceId && { sourceId: data.sourceId },
694
- ...data.sourceModel && { sourceModel: data.sourceModel },
695
- metadata: {
696
- ...metadata,
697
- planKey,
698
- entity,
699
- monetizationType,
700
- paymentIntentId: paymentIntent.id
701
- },
702
- idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
703
- });
704
- }
705
- let subscription = null;
706
- if (this.models.Subscription) {
707
- const SubscriptionModel = this.models.Subscription;
708
- subscription = (await this.plugins.executeHook("subscription.create.before", this.getPluginContext(idempotencyKey), {
709
- subscriptionId: void 0,
710
- planKey,
711
- customerId: data.customerId,
712
- organizationId: data.organizationId,
713
- entity
714
- }, async () => {
715
- const subscriptionData = {
716
- organizationId: data.organizationId,
717
- customerId: data.customerId ?? null,
718
- planKey,
719
- amount,
720
- currency,
721
- status: isFree ? "active" : "pending",
722
- isActive: isFree,
723
- gateway,
724
- transactionId: transaction?._id ?? null,
725
- paymentIntentId: paymentIntent?.id ?? null,
726
- metadata: {
727
- ...metadata,
728
- isFree,
729
- entity,
730
- monetizationType
731
- },
732
- ...data
733
- };
734
- delete subscriptionData.sourceId;
735
- delete subscriptionData.sourceModel;
736
- const sub = await SubscriptionModel.create(subscriptionData);
737
- await this.plugins.executeHook("subscription.create.after", this.getPluginContext(idempotencyKey), {
738
- subscriptionId: sub._id.toString(),
739
- planKey,
740
- customerId: data.customerId,
741
- organizationId: data.organizationId,
742
- entity
743
- }, async () => ({
744
- subscription: sub,
745
- transaction
746
- }));
747
- return {
748
- subscription: sub,
749
- transaction
750
- };
751
- })).subscription;
752
- }
753
- this.events.emit("monetization.created", {
754
- monetizationType,
755
- subscription: subscription ?? void 0,
756
- transaction: transaction ?? void 0,
757
- paymentIntent: paymentIntent ?? void 0
758
- });
759
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
760
- if (transaction) this.events.emit("purchase.created", {
761
- monetizationType,
762
- subscription: subscription ?? void 0,
763
- transaction,
764
- paymentIntent: paymentIntent ?? void 0
765
- });
766
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
767
- if (subscription) this.events.emit("subscription.created", {
768
- subscriptionId: subscription._id.toString(),
769
- subscription,
770
- transactionId: transaction?._id?.toString()
771
- });
772
- } else if (monetizationType === MONETIZATION_TYPES.FREE) this.events.emit("free.created", {
773
- monetizationType,
774
- subscription: subscription ?? void 0,
775
- transaction: transaction ?? void 0,
776
- paymentIntent: paymentIntent ?? void 0
777
- });
778
- const result = {
779
- subscription,
780
- transaction,
781
- paymentIntent
782
- };
783
- return this.plugins.executeHook("monetization.create.after", this.getPluginContext(params.idempotencyKey), params, async () => result);
784
- });
785
- }
786
- /**
787
- * Activate subscription after payment verification
788
- *
789
- * @param subscriptionId - Subscription ID or transaction ID
790
- * @param options - Activation options
791
- * @returns Updated subscription
792
- */
793
- async activate(subscriptionId, options = {}) {
794
- return this.plugins.executeHook("subscription.activate.before", this.getPluginContext(), {
795
- subscriptionId,
796
- ...options
797
- }, async () => {
798
- const { timestamp = /* @__PURE__ */ new Date() } = options;
799
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
800
- const subscription = await this.models.Subscription.findById(subscriptionId);
801
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
802
- if (subscription.isActive) {
803
- this.logger.warn("Subscription already active", { subscriptionId });
804
- return subscription;
805
- }
806
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
807
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(subscription.status, "active", subscription._id.toString(), {
808
- changedBy: "system",
809
- reason: `Subscription activated for plan: ${subscription.planKey}`,
810
- metadata: {
811
- planKey: subscription.planKey,
812
- startDate: timestamp,
813
- endDate: periodEnd
814
- }
815
- });
816
- subscription.isActive = true;
817
- subscription.status = "active";
818
- subscription.startDate = timestamp;
819
- subscription.endDate = periodEnd;
820
- subscription.activatedAt = timestamp;
821
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
822
- await subscription.save();
823
- this.events.emit("subscription.activated", {
824
- subscription,
825
- activatedAt: timestamp
826
- });
827
- return this.plugins.executeHook("subscription.activate.after", this.getPluginContext(), {
828
- subscriptionId,
829
- ...options
830
- }, async () => subscription);
831
- });
832
- }
833
- /**
834
- * Renew subscription
835
- *
836
- * @param subscriptionId - Subscription ID
837
- * @param params - Renewal parameters
838
- * @returns { subscription, transaction, paymentIntent }
839
- */
840
- async renew(subscriptionId, params = {}) {
841
- const { gateway = "manual", entity = null, paymentData, metadata = {}, idempotencyKey = null } = params;
842
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
843
- const subscription = await this.models.Subscription.findById(subscriptionId);
844
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
845
- if (subscription.amount === 0) throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
846
- const provider = this.providers[gateway];
847
- if (!provider) throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
848
- let paymentIntent = null;
849
- try {
850
- paymentIntent = await provider.createIntent({
851
- amount: subscription.amount,
852
- currency: subscription.currency ?? "BDT",
853
- metadata: {
854
- ...metadata,
855
- type: "subscription_renewal",
856
- subscriptionId: subscription._id.toString()
857
- }
858
- });
859
- } catch (error) {
860
- this.logger.error("Failed to create payment intent for renewal:", error);
861
- throw new PaymentIntentCreationError(gateway, error);
862
- }
863
- const effectiveEntity = entity ?? subscription.metadata?.entity;
864
- const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
865
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
866
- const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_FLOW.INFLOW;
867
- const commissionRate = getCommissionRate(this.config, category);
868
- const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
869
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
870
- const feeAmount = commission?.gatewayFeeAmount || 0;
871
- const netAmount = subscription.amount - feeAmount;
872
- const transaction = await this.models.Transaction.create({
873
- organizationId: subscription.organizationId,
874
- customerId: subscription.customerId,
875
- type: category,
876
- flow: transactionFlow,
877
- tags: [
878
- "recurring",
879
- "subscription",
880
- "renewal"
881
- ],
882
- amount: subscription.amount,
883
- currency: subscription.currency ?? "BDT",
884
- fee: feeAmount,
885
- tax: 0,
886
- net: netAmount,
887
- method: paymentData?.method ?? "manual",
888
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
889
- gateway: {
890
- provider: gateway,
891
- sessionId: paymentIntent.sessionId,
892
- paymentIntentId: paymentIntent.paymentIntentId,
893
- chargeId: paymentIntent.id,
894
- metadata: paymentIntent.metadata
895
- },
896
- paymentDetails: {
897
- provider: gateway,
898
- ...paymentData
899
- },
900
- ...commission && { commission },
901
- sourceId: subscription._id,
902
- sourceModel: "Subscription",
903
- metadata: {
904
- ...metadata,
905
- subscriptionId: subscription._id.toString(),
906
- entity: effectiveEntity,
907
- monetizationType: effectiveMonetizationType,
908
- isRenewal: true,
909
- paymentIntentId: paymentIntent.id
910
- },
911
- idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
912
- });
913
- subscription.status = "pending_renewal";
914
- subscription.renewalTransactionId = transaction._id;
915
- subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
916
- await subscription.save();
917
- this.events.emit("subscription.renewed", {
918
- subscription,
919
- transaction,
920
- paymentIntent: paymentIntent ?? void 0,
921
- renewalCount: subscription.renewalCount
922
- });
923
- return {
924
- subscription,
925
- transaction,
926
- paymentIntent
927
- };
928
- }
929
- /**
930
- * Cancel subscription
931
- *
932
- * @param subscriptionId - Subscription ID
933
- * @param options - Cancellation options
934
- * @returns Updated subscription
935
- */
936
- async cancel(subscriptionId, options = {}) {
937
- return this.plugins.executeHook("subscription.cancel.before", this.getPluginContext(), {
938
- subscriptionId,
939
- ...options
940
- }, async () => {
941
- const { immediate = false, reason = null } = options;
942
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
943
- const subscription = await this.models.Subscription.findById(subscriptionId);
944
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
945
- const now = /* @__PURE__ */ new Date();
946
- if (immediate) {
947
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(subscription.status, "cancelled", subscription._id.toString(), {
948
- changedBy: "system",
949
- reason: `Subscription cancelled immediately${reason ? ": " + reason : ""}`,
950
- metadata: {
951
- cancellationReason: reason,
952
- immediate: true
953
- }
954
- });
955
- subscription.isActive = false;
956
- subscription.status = "cancelled";
957
- subscription.canceledAt = now;
958
- subscription.cancellationReason = reason;
959
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
960
- } else {
961
- subscription.cancelAt = subscription.endDate ?? now;
962
- subscription.cancellationReason = reason;
963
- }
964
- await subscription.save();
965
- this.events.emit("subscription.cancelled", {
966
- subscription,
967
- immediate,
968
- reason: reason ?? void 0,
969
- canceledAt: immediate ? now : subscription.cancelAt
970
- });
971
- return this.plugins.executeHook("subscription.cancel.after", this.getPluginContext(), {
972
- subscriptionId,
973
- ...options
974
- }, async () => subscription);
975
- });
976
- }
977
- /**
978
- * Pause subscription
979
- *
980
- * @param subscriptionId - Subscription ID
981
- * @param options - Pause options
982
- * @returns Updated subscription
983
- */
984
- async pause(subscriptionId, options = {}) {
985
- return this.plugins.executeHook("subscription.pause.before", this.getPluginContext(), {
986
- subscriptionId,
987
- ...options
988
- }, async () => {
989
- const { reason = null } = options;
990
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
991
- const subscription = await this.models.Subscription.findById(subscriptionId);
992
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
993
- if (!subscription.isActive) throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
994
- const pausedAt = /* @__PURE__ */ new Date();
995
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(subscription.status, "paused", subscription._id.toString(), {
996
- changedBy: "system",
997
- reason: `Subscription paused${reason ? ": " + reason : ""}`,
998
- metadata: {
999
- pauseReason: reason,
1000
- pausedAt
1001
- }
1002
- });
1003
- subscription.isActive = false;
1004
- subscription.status = "paused";
1005
- subscription.pausedAt = pausedAt;
1006
- subscription.pauseReason = reason;
1007
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1008
- await subscription.save();
1009
- this.events.emit("subscription.paused", {
1010
- subscription,
1011
- reason: reason ?? void 0,
1012
- pausedAt
1013
- });
1014
- return this.plugins.executeHook("subscription.pause.after", this.getPluginContext(), {
1015
- subscriptionId,
1016
- ...options
1017
- }, async () => subscription);
1018
- });
1019
- }
1020
- /**
1021
- * Resume subscription
1022
- *
1023
- * @param subscriptionId - Subscription ID
1024
- * @param options - Resume options
1025
- * @returns Updated subscription
1026
- */
1027
- async resume(subscriptionId, options = {}) {
1028
- return this.plugins.executeHook("subscription.resume.before", this.getPluginContext(), {
1029
- subscriptionId,
1030
- ...options
1031
- }, async () => {
1032
- const { extendPeriod = false } = options;
1033
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
1034
- const subscription = await this.models.Subscription.findById(subscriptionId);
1035
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
1036
- if (!subscription.pausedAt) throw new InvalidStateTransitionError("resume", "paused", subscription.status, "Only paused subscriptions can be resumed");
1037
- const now = /* @__PURE__ */ new Date();
1038
- const pausedAt = new Date(subscription.pausedAt);
1039
- const pauseDuration = now.getTime() - pausedAt.getTime();
1040
- const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(subscription.status, "active", subscription._id.toString(), {
1041
- changedBy: "system",
1042
- reason: "Subscription resumed from paused state",
1043
- metadata: {
1044
- pausedAt,
1045
- pauseDuration,
1046
- extendPeriod,
1047
- newEndDate: extendPeriod && subscription.endDate ? new Date(new Date(subscription.endDate).getTime() + pauseDuration) : void 0
1048
- }
1049
- });
1050
- subscription.isActive = true;
1051
- subscription.status = "active";
1052
- subscription.pausedAt = null;
1053
- subscription.pauseReason = null;
1054
- if (extendPeriod && subscription.endDate) {
1055
- const currentEnd = new Date(subscription.endDate);
1056
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1057
- }
1058
- Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1059
- await subscription.save();
1060
- this.events.emit("subscription.resumed", {
1061
- subscription,
1062
- extendPeriod,
1063
- pauseDuration,
1064
- resumedAt: now
1065
- });
1066
- return this.plugins.executeHook("subscription.resume.after", this.getPluginContext(), {
1067
- subscriptionId,
1068
- ...options
1069
- }, async () => subscription);
1070
- });
1071
- }
1072
- /**
1073
- * List subscriptions with filters
1074
- *
1075
- * @param filters - Query filters
1076
- * @param options - Query options (limit, skip, sort)
1077
- * @returns Subscriptions
1078
- */
1079
- async list(filters = {}, options = {}) {
1080
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
1081
- const SubscriptionModel = this.models.Subscription;
1082
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1083
- return await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
1084
- }
1085
- /**
1086
- * Get subscription by ID
1087
- *
1088
- * @param subscriptionId - Subscription ID
1089
- * @returns Subscription
1090
- */
1091
- async get(subscriptionId) {
1092
- if (!this.models.Subscription) throw new ModelNotRegisteredError("Subscription");
1093
- const subscription = await this.models.Subscription.findById(subscriptionId);
1094
- if (!subscription) throw new SubscriptionNotFoundError(subscriptionId);
1095
- return subscription;
1096
- }
1097
- /**
1098
- * Calculate period end date based on plan key
1099
- * @private
1100
- */
1101
- _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
1102
- const start = new Date(startDate);
1103
- const end = new Date(start);
1104
- switch (planKey) {
1105
- case "monthly":
1106
- end.setMonth(end.getMonth() + 1);
1107
- break;
1108
- case "quarterly":
1109
- end.setMonth(end.getMonth() + 3);
1110
- break;
1111
- case "yearly":
1112
- end.setFullYear(end.getFullYear() + 1);
1113
- break;
1114
- default: end.setDate(end.getDate() + 30);
1115
- }
1116
- return end;
1117
- }
1118
- };
1119
-
1120
- //#endregion
1121
- //#region src/application/services/payment.service.ts
1122
- /**
1123
- * Payment Service
1124
- * @classytic/revenue
1125
- *
1126
- * Framework-agnostic payment verification and management service with DI
1127
- * Handles payment verification, refunds, and status updates
1128
- */
1129
- /**
1130
- * Payment Service
1131
- * Uses DI container for all dependencies
1132
- *
1133
- * Architecture:
1134
- * - PluginManager: Wraps operations with lifecycle hooks (before/after)
1135
- * - EventBus: Fire-and-forget notifications for completed operations
1136
- */
1137
- var PaymentService = class {
1138
- models;
1139
- providers;
1140
- config;
1141
- plugins;
1142
- logger;
1143
- events;
1144
- retryConfig;
1145
- circuitBreaker;
1146
- constructor(container) {
1147
- this.models = container.get("models");
1148
- this.providers = container.get("providers");
1149
- this.config = container.get("config");
1150
- this.plugins = container.get("plugins");
1151
- this.logger = container.get("logger");
1152
- this.events = container.get("events");
1153
- this.retryConfig = container.get("retryConfig");
1154
- this.circuitBreaker = container.get("circuitBreaker");
1155
- }
1156
- /**
1157
- * Create plugin context for hook execution
1158
- * @private
1159
- */
1160
- getPluginContext(idempotencyKey) {
1161
- return {
1162
- events: this.events,
1163
- logger: this.logger,
1164
- storage: /* @__PURE__ */ new Map(),
1165
- meta: {
1166
- idempotencyKey,
1167
- requestId: nanoid(),
1168
- timestamp: /* @__PURE__ */ new Date()
1169
- }
1170
- };
1171
- }
1172
- /**
1173
- * Execute provider call with retry and circuit breaker protection
1174
- * @private
1175
- */
1176
- async executeProviderCall(operation, operationName) {
1177
- const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
1178
- if (this.retryConfig && Object.keys(this.retryConfig).length > 0) return retry(withCircuitBreaker, {
1179
- ...this.retryConfig,
1180
- onRetry: (error, attempt, delay) => {
1181
- this.logger.warn(`[${operationName}] Retry attempt ${attempt} after ${delay}ms:`, error);
1182
- this.retryConfig.onRetry?.(error, attempt, delay);
1183
- }
1184
- });
1185
- return withCircuitBreaker();
1186
- }
1187
- /**
1188
- * Verify a payment
1189
- *
1190
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1191
- * @param options - Verification options
1192
- * @returns { transaction, status }
1193
- */
1194
- async verify(paymentIntentId, options = {}) {
1195
- return this.plugins.executeHook("payment.verify.before", this.getPluginContext(), {
1196
- id: paymentIntentId,
1197
- ...options
1198
- }, async () => {
1199
- const { verifiedBy = null } = options;
1200
- const TransactionModel = this.models.Transaction;
1201
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1202
- if (!transaction) throw new TransactionNotFoundError(paymentIntentId);
1203
- if (transaction.status === "verified" || transaction.status === "completed") throw new AlreadyVerifiedError(transaction._id.toString());
1204
- const gatewayType = transaction.gateway?.type ?? "manual";
1205
- const provider = this.providers[gatewayType];
1206
- if (!provider) throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1207
- let paymentResult = null;
1208
- try {
1209
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
1210
- paymentResult = await this.executeProviderCall(() => provider.verifyPayment(actualIntentId), `${gatewayType}.verifyPayment`);
1211
- } catch (error) {
1212
- this.logger.error("Payment verification failed:", error);
1213
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, "failed", transaction._id.toString(), {
1214
- changedBy: "system",
1215
- reason: `Payment verification failed: ${error.message}`,
1216
- metadata: { error: error.message }
1217
- });
1218
- transaction.status = "failed";
1219
- transaction.failureReason = error.message;
1220
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1221
- transaction.metadata = {
1222
- ...transaction.metadata,
1223
- verificationError: error.message,
1224
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
1225
- };
1226
- await transaction.save();
1227
- this.events.emit("payment.failed", {
1228
- transaction,
1229
- error: error.message,
1230
- provider: gatewayType,
1231
- paymentIntentId
1232
- });
1233
- throw new PaymentVerificationError(paymentIntentId, error.message);
1234
- }
1235
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) throw new ValidationError(`Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`, {
1236
- expected: transaction.amount,
1237
- actual: paymentResult.amount
1238
- });
1239
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) throw new ValidationError(`Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`, {
1240
- expected: transaction.currency,
1241
- actual: paymentResult.currency
1242
- });
1243
- const newStatus = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1244
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, newStatus, transaction._id.toString(), {
1245
- changedBy: verifiedBy ?? "system",
1246
- reason: `Payment verification ${paymentResult.status === "succeeded" ? "succeeded" : "resulted in status: " + newStatus}`,
1247
- metadata: { paymentResult: paymentResult.metadata }
1248
- });
1249
- transaction.status = newStatus;
1250
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1251
- transaction.verifiedBy = verifiedBy;
1252
- transaction.gateway = {
1253
- ...transaction.gateway,
1254
- type: transaction.gateway?.type ?? "manual",
1255
- verificationData: paymentResult.metadata
1256
- };
1257
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1258
- await transaction.save();
1259
- if (newStatus === "verified") this.events.emit("payment.verified", {
1260
- transaction,
1261
- paymentResult,
1262
- verifiedBy: verifiedBy || void 0
1263
- });
1264
- else if (newStatus === "failed") this.events.emit("payment.failed", {
1265
- transaction,
1266
- error: paymentResult.metadata?.errorMessage || "Payment verification failed",
1267
- provider: gatewayType,
1268
- paymentIntentId: transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId
1269
- });
1270
- else if (newStatus === "requires_action") this.events.emit("payment.requires_action", {
1271
- transaction,
1272
- paymentResult,
1273
- action: paymentResult.metadata?.requiredAction
1274
- });
1275
- else if (newStatus === "processing") this.events.emit("payment.processing", {
1276
- transaction,
1277
- paymentResult
1278
- });
1279
- const result = {
1280
- transaction,
1281
- paymentResult,
1282
- status: transaction.status
1283
- };
1284
- return this.plugins.executeHook("payment.verify.after", this.getPluginContext(), {
1285
- id: paymentIntentId,
1286
- ...options
1287
- }, async () => result);
1288
- });
1289
- }
1290
- /**
1291
- * Get payment status
1292
- *
1293
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1294
- * @returns { transaction, status }
1295
- */
1296
- async getStatus(paymentIntentId) {
1297
- const TransactionModel = this.models.Transaction;
1298
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1299
- if (!transaction) throw new TransactionNotFoundError(paymentIntentId);
1300
- const gatewayType = transaction.gateway?.type ?? "manual";
1301
- const provider = this.providers[gatewayType];
1302
- if (!provider) throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1303
- let paymentResult = null;
1304
- try {
1305
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
1306
- paymentResult = await this.executeProviderCall(() => provider.getStatus(actualIntentId), `${gatewayType}.getStatus`);
1307
- } catch (error) {
1308
- this.logger.warn("Failed to get payment status from provider:", error);
1309
- return {
1310
- transaction,
1311
- status: transaction.status,
1312
- provider: gatewayType
1313
- };
1314
- }
1315
- return {
1316
- transaction,
1317
- paymentResult,
1318
- status: paymentResult.status,
1319
- provider: gatewayType
1320
- };
1321
- }
1322
- /**
1323
- * Refund a payment
1324
- *
1325
- * @param paymentId - Payment intent ID, session ID, or transaction ID
1326
- * @param amount - Amount to refund (optional, full refund if not provided)
1327
- * @param options - Refund options
1328
- * @returns { transaction, refundResult }
1329
- */
1330
- async refund(paymentId, amount = null, options = {}) {
1331
- return this.plugins.executeHook("payment.refund.before", this.getPluginContext(), {
1332
- transactionId: paymentId,
1333
- amount,
1334
- ...options
1335
- }, async () => {
1336
- const { reason = null } = options;
1337
- const TransactionModel = this.models.Transaction;
1338
- const transaction = await this._findTransaction(TransactionModel, paymentId);
1339
- if (!transaction) throw new TransactionNotFoundError(paymentId);
1340
- if (transaction.status !== "verified" && transaction.status !== "completed" && transaction.status !== "partially_refunded") throw new InvalidStateTransitionError("transaction", transaction._id.toString(), transaction.status, "verified, completed, or partially_refunded");
1341
- const gatewayType = transaction.gateway?.type ?? "manual";
1342
- const provider = this.providers[gatewayType];
1343
- if (!provider) throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1344
- if (!provider.getCapabilities().supportsRefunds) throw new RefundNotSupportedError(gatewayType);
1345
- const refundedSoFar = transaction.refundedAmount ?? 0;
1346
- const refundableAmount = transaction.amount - refundedSoFar;
1347
- const refundAmount = amount ?? refundableAmount;
1348
- if (refundAmount <= 0) throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1349
- if (refundAmount > refundableAmount) throw new ValidationError(`Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`, {
1350
- refundAmount,
1351
- refundableAmount,
1352
- alreadyRefunded: refundedSoFar
1353
- });
1354
- let refundResult;
1355
- try {
1356
- const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentId;
1357
- refundResult = await this.executeProviderCall(() => provider.refund(actualIntentId, refundAmount, { reason: reason ?? void 0 }), `${gatewayType}.refund`);
1358
- } catch (error) {
1359
- this.logger.error("Refund failed:", error);
1360
- throw new RefundError(paymentId, error.message);
1361
- }
1362
- const refundFlow = this.config.transactionTypeMapping?.refund ?? TRANSACTION_FLOW.OUTFLOW;
1363
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1364
- let refundTaxAmount = 0;
1365
- if (transaction.tax && transaction.tax > 0 && transaction.amount > 0) {
1366
- const ratio = refundAmount / transaction.amount;
1367
- refundTaxAmount = Math.round(transaction.tax * ratio);
1368
- }
1369
- const refundFeeAmount = refundCommission?.gatewayFeeAmount || 0;
1370
- const refundNetAmount = refundAmount - refundFeeAmount - refundTaxAmount;
1371
- const refundTransaction = await TransactionModel.create({
1372
- organizationId: transaction.organizationId,
1373
- customerId: transaction.customerId,
1374
- type: "refund",
1375
- flow: refundFlow,
1376
- tags: ["refund"],
1377
- amount: refundAmount,
1378
- currency: transaction.currency,
1379
- fee: refundFeeAmount,
1380
- tax: refundTaxAmount,
1381
- net: refundNetAmount,
1382
- ...transaction.taxDetails && { taxDetails: transaction.taxDetails },
1383
- method: transaction.method ?? "manual",
1384
- status: "completed",
1385
- gateway: {
1386
- provider: transaction.gateway?.provider ?? "manual",
1387
- paymentIntentId: refundResult.id,
1388
- chargeId: refundResult.id
1389
- },
1390
- paymentDetails: transaction.paymentDetails,
1391
- ...refundCommission && { commission: refundCommission },
1392
- ...transaction.sourceId && { sourceId: transaction.sourceId },
1393
- ...transaction.sourceModel && { sourceModel: transaction.sourceModel },
1394
- relatedTransactionId: transaction._id,
1395
- metadata: {
1396
- ...transaction.metadata,
1397
- isRefund: true,
1398
- originalTransactionId: transaction._id.toString(),
1399
- refundReason: reason,
1400
- refundResult: refundResult.metadata
1401
- },
1402
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1403
- });
1404
- const isPartialRefund = refundAmount < refundableAmount;
1405
- const refundStatus = isPartialRefund ? "partially_refunded" : "refunded";
1406
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, refundStatus, transaction._id.toString(), {
1407
- changedBy: "system",
1408
- reason: `Refund processed: ${isPartialRefund ? "partial" : "full"} refund of ${refundAmount}${reason ? " - " + reason : ""}`,
1409
- metadata: {
1410
- refundAmount,
1411
- isPartialRefund,
1412
- refundTransactionId: refundTransaction._id.toString()
1413
- }
1414
- });
1415
- transaction.status = refundStatus;
1416
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1417
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1418
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1419
- transaction.metadata = {
1420
- ...transaction.metadata,
1421
- refundTransactionId: refundTransaction._id.toString(),
1422
- refundReason: reason
1423
- };
1424
- await transaction.save();
1425
- this.events.emit("payment.refunded", {
1426
- transaction,
1427
- refundTransaction,
1428
- refundResult: {
1429
- ...refundResult,
1430
- currency: refundResult.currency ?? "USD",
1431
- metadata: refundResult.metadata ?? {}
1432
- },
1433
- refundAmount,
1434
- reason: reason ?? void 0,
1435
- isPartialRefund
1436
- });
1437
- const result = {
1438
- transaction,
1439
- refundTransaction,
1440
- refundResult,
1441
- status: transaction.status
1442
- };
1443
- return this.plugins.executeHook("payment.refund.after", this.getPluginContext(), {
1444
- transactionId: paymentId,
1445
- amount,
1446
- ...options
1447
- }, async () => result);
1448
- });
1449
- }
1450
- /**
1451
- * Handle webhook from payment provider
1452
- *
1453
- * @param provider - Provider name
1454
- * @param payload - Webhook payload
1455
- * @param headers - Request headers
1456
- * @returns { event, transaction }
1457
- */
1458
- async handleWebhook(providerName, payload, headers = {}) {
1459
- const provider = this.providers[providerName];
1460
- if (!provider) throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1461
- if (!provider.getCapabilities().supportsWebhooks) throw new ProviderCapabilityError(providerName, "webhooks");
1462
- let webhookEvent;
1463
- try {
1464
- webhookEvent = await this.executeProviderCall(() => provider.handleWebhook(payload, headers), `${providerName}.handleWebhook`);
1465
- } catch (error) {
1466
- this.logger.error("Webhook processing failed:", error);
1467
- throw new ProviderError(`Webhook processing failed for ${providerName}: ${error.message}`, "WEBHOOK_PROCESSING_FAILED", { retryable: false });
1468
- }
1469
- if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) throw new ValidationError(`Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`, {
1470
- provider: providerName,
1471
- eventType: webhookEvent?.type
1472
- });
1473
- const TransactionModel = this.models.Transaction;
1474
- let transaction = null;
1475
- if (webhookEvent.data.sessionId) transaction = await TransactionModel.findOne({ "gateway.sessionId": webhookEvent.data.sessionId });
1476
- if (!transaction && webhookEvent.data.paymentIntentId) transaction = await TransactionModel.findOne({ "gateway.paymentIntentId": webhookEvent.data.paymentIntentId });
1477
- if (!transaction) {
1478
- this.logger.warn("Transaction not found for webhook event", {
1479
- provider: providerName,
1480
- eventId: webhookEvent.id,
1481
- sessionId: webhookEvent.data.sessionId,
1482
- paymentIntentId: webhookEvent.data.paymentIntentId
1483
- });
1484
- throw new TransactionNotFoundError(webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown");
1485
- }
1486
- if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) transaction.gateway = {
1487
- ...transaction.gateway,
1488
- type: transaction.gateway?.type ?? "manual",
1489
- sessionId: webhookEvent.data.sessionId
1490
- };
1491
- if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) transaction.gateway = {
1492
- ...transaction.gateway,
1493
- type: transaction.gateway?.type ?? "manual",
1494
- paymentIntentId: webhookEvent.data.paymentIntentId
1495
- };
1496
- if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
1497
- this.logger.warn("Webhook already processed", {
1498
- transactionId: transaction._id,
1499
- eventId: webhookEvent.id
1500
- });
1501
- return {
1502
- event: webhookEvent,
1503
- transaction,
1504
- status: "already_processed"
1505
- };
1506
- }
1507
- transaction.webhook = {
1508
- eventId: webhookEvent.id,
1509
- eventType: webhookEvent.type,
1510
- receivedAt: /* @__PURE__ */ new Date(),
1511
- processedAt: /* @__PURE__ */ new Date(),
1512
- data: webhookEvent.data
1513
- };
1514
- let newStatus = transaction.status;
1515
- if (webhookEvent.type === "payment.succeeded") newStatus = "verified";
1516
- else if (webhookEvent.type === "payment.failed") newStatus = "failed";
1517
- else if (webhookEvent.type === "refund.succeeded") newStatus = "refunded";
1518
- if (newStatus !== transaction.status) {
1519
- const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, newStatus, transaction._id.toString(), {
1520
- changedBy: "webhook",
1521
- reason: `Webhook event: ${webhookEvent.type}`,
1522
- metadata: {
1523
- webhookId: webhookEvent.id,
1524
- webhookType: webhookEvent.type,
1525
- webhookData: webhookEvent.data
1526
- }
1527
- });
1528
- transaction.status = newStatus;
1529
- if (newStatus === "verified") transaction.verifiedAt = webhookEvent.createdAt;
1530
- else if (newStatus === "refunded") transaction.refundedAt = webhookEvent.createdAt;
1531
- else if (newStatus === "failed") transaction.failedAt = webhookEvent.createdAt;
1532
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1533
- }
1534
- await transaction.save();
1535
- this.events.emit("webhook.processed", {
1536
- webhookType: webhookEvent.type,
1537
- provider: webhookEvent.provider,
1538
- event: webhookEvent,
1539
- transaction,
1540
- processedAt: /* @__PURE__ */ new Date()
1541
- });
1542
- return {
1543
- event: webhookEvent,
1544
- transaction,
1545
- status: "processed"
1546
- };
1547
- }
1548
- /**
1549
- * List payments/transactions with filters
1550
- *
1551
- * @param filters - Query filters
1552
- * @param options - Query options (limit, skip, sort)
1553
- * @returns Transactions
1554
- */
1555
- async list(filters = {}, options = {}) {
1556
- const TransactionModel = this.models.Transaction;
1557
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1558
- return await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
1559
- }
1560
- /**
1561
- * Get payment/transaction by ID
1562
- *
1563
- * @param transactionId - Transaction ID
1564
- * @returns Transaction
1565
- */
1566
- async get(transactionId) {
1567
- const transaction = await this.models.Transaction.findById(transactionId);
1568
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1569
- return transaction;
1570
- }
1571
- /**
1572
- * Get provider instance
1573
- *
1574
- * @param providerName - Provider name
1575
- * @returns Provider instance
1576
- */
1577
- getProvider(providerName) {
1578
- const provider = this.providers[providerName];
1579
- if (!provider) throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1580
- return provider;
1581
- }
1582
- /**
1583
- * Find transaction by sessionId, paymentIntentId, or transaction ID
1584
- * @private
1585
- */
1586
- async _findTransaction(TransactionModel, identifier) {
1587
- let transaction = await TransactionModel.findOne({ "gateway.sessionId": identifier });
1588
- if (!transaction) transaction = await TransactionModel.findOne({ "gateway.paymentIntentId": identifier });
1589
- if (!transaction) transaction = await TransactionModel.findById(identifier);
1590
- return transaction;
1591
- }
1592
- };
1593
-
1594
- //#endregion
1595
- //#region src/application/services/transaction.service.ts
1596
- /**
1597
- * Transaction Service
1598
- * @classytic/revenue
1599
- *
1600
- * Thin, focused transaction service for core operations
1601
- * Users handle their own analytics, exports, and complex queries
1602
- *
1603
- * Works with ANY model implementation:
1604
- * - Plain Mongoose models
1605
- * - @classytic/mongokit Repository instances
1606
- * - Any other abstraction with compatible interface
1607
- */
1608
- /**
1609
- * Transaction Service
1610
- * Focused on core transaction lifecycle operations
1611
- *
1612
- * Architecture:
1613
- * - PluginManager: Wraps operations with lifecycle hooks (before/after)
1614
- * - EventBus: Fire-and-forget notifications for completed operations
1615
- */
1616
- var TransactionService = class {
1617
- models;
1618
- plugins;
1619
- events;
1620
- logger;
1621
- constructor(container) {
1622
- this.models = container.get("models");
1623
- this.plugins = container.get("plugins");
1624
- this.events = container.get("events");
1625
- this.logger = container.get("logger");
1626
- }
1627
- /**
1628
- * Create plugin context for hook execution
1629
- * @private
1630
- */
1631
- getPluginContext() {
1632
- return {
1633
- events: this.events,
1634
- logger: this.logger,
1635
- storage: /* @__PURE__ */ new Map(),
1636
- meta: {
1637
- requestId: nanoid(),
1638
- timestamp: /* @__PURE__ */ new Date()
1639
- }
1640
- };
1641
- }
1642
- /**
1643
- * Get transaction by ID
1644
- *
1645
- * @param transactionId - Transaction ID
1646
- * @returns Transaction
1647
- */
1648
- async get(transactionId) {
1649
- const transaction = await this.models.Transaction.findById(transactionId);
1650
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1651
- return transaction;
1652
- }
1653
- /**
1654
- * List transactions with filters
1655
- *
1656
- * @param filters - Query filters
1657
- * @param options - Query options (limit, skip, sort, populate)
1658
- * @returns { transactions, total, page, limit }
1659
- */
1660
- async list(filters = {}, options = {}) {
1661
- const TransactionModel = this.models.Transaction;
1662
- const { limit = 50, skip = 0, page = null, sort = { createdAt: -1 }, populate = [] } = options;
1663
- const actualSkip = page ? (page - 1) * limit : skip;
1664
- let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
1665
- if (populate.length > 0 && typeof query.populate === "function") populate.forEach((field) => {
1666
- query = query.populate(field);
1667
- });
1668
- const transactions = await query;
1669
- const model = TransactionModel;
1670
- const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
1671
- return {
1672
- transactions,
1673
- total,
1674
- page: page ?? Math.floor(actualSkip / limit) + 1,
1675
- limit,
1676
- pages: Math.ceil(total / limit)
1677
- };
1678
- }
1679
- /**
1680
- * Update transaction
1681
- *
1682
- * @param transactionId - Transaction ID
1683
- * @param updates - Fields to update
1684
- * @returns Updated transaction
1685
- */
1686
- async update(transactionId, updates) {
1687
- const hookInput = {
1688
- transactionId,
1689
- updates
1690
- };
1691
- return this.plugins.executeHook("transaction.update.before", this.getPluginContext(), hookInput, async () => {
1692
- const TransactionModel = this.models.Transaction;
1693
- const effectiveUpdates = hookInput.updates;
1694
- const model = TransactionModel;
1695
- let transaction;
1696
- if (typeof model.update === "function") transaction = await model.update(transactionId, effectiveUpdates);
1697
- else if (typeof model.findByIdAndUpdate === "function") transaction = await model.findByIdAndUpdate(transactionId, { $set: effectiveUpdates }, { new: true });
1698
- else throw new Error("Transaction model does not support update operations");
1699
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1700
- this.events.emit("transaction.updated", {
1701
- transaction,
1702
- updates: effectiveUpdates
1703
- });
1704
- return this.plugins.executeHook("transaction.update.after", this.getPluginContext(), {
1705
- transactionId,
1706
- updates: effectiveUpdates
1707
- }, async () => transaction);
1708
- });
1709
- }
1710
- };
1711
-
1712
- //#endregion
1713
- //#region src/application/services/escrow.service.ts
1714
- /**
1715
- * Escrow Service
1716
- * @classytic/revenue
1717
- *
1718
- * Platform-as-intermediary payment flow
1719
- * Hold funds → Verify → Split/Deduct → Release to organization
1720
- */
1721
- /**
1722
- * Escrow Service
1723
- * Uses DI container for all dependencies
1724
- *
1725
- * Architecture:
1726
- * - PluginManager: Wraps operations with lifecycle hooks (before/after)
1727
- * - EventBus: Fire-and-forget notifications for completed operations
1728
- */
1729
- var EscrowService = class {
1730
- models;
1731
- plugins;
1732
- logger;
1733
- events;
1734
- constructor(container) {
1735
- this.models = container.get("models");
1736
- this.plugins = container.get("plugins");
1737
- this.logger = container.get("logger");
1738
- this.events = container.get("events");
1739
- }
1740
- /**
1741
- * Create plugin context for hook execution
1742
- * @private
1743
- */
1744
- getPluginContext() {
1745
- return {
1746
- events: this.events,
1747
- logger: this.logger,
1748
- storage: /* @__PURE__ */ new Map(),
1749
- meta: {
1750
- requestId: nanoid(),
1751
- timestamp: /* @__PURE__ */ new Date()
1752
- }
1753
- };
1754
- }
1755
- /**
1756
- * Hold funds in escrow
1757
- *
1758
- * @param transactionId - Transaction to hold
1759
- * @param options - Hold options
1760
- * @returns Updated transaction
1761
- */
1762
- async hold(transactionId, options = {}) {
1763
- return this.plugins.executeHook("escrow.hold.before", this.getPluginContext(), {
1764
- transactionId,
1765
- ...options
1766
- }, async () => {
1767
- const { reason = HOLD_REASON.PAYMENT_VERIFICATION, holdUntil = null, metadata = {} } = options;
1768
- const transaction = await this.models.Transaction.findById(transactionId);
1769
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1770
- if (transaction.status !== TRANSACTION_STATUS.VERIFIED) throw new InvalidStateTransitionError("transaction", transaction._id.toString(), transaction.status, TRANSACTION_STATUS.VERIFIED);
1771
- const heldAmount = transaction.amount;
1772
- transaction.hold = {
1773
- status: HOLD_STATUS.HELD,
1774
- heldAmount,
1775
- releasedAmount: 0,
1776
- reason,
1777
- heldAt: /* @__PURE__ */ new Date(),
1778
- ...holdUntil && { holdUntil },
1779
- releases: [],
1780
- metadata
1781
- };
1782
- await transaction.save();
1783
- this.events.emit("escrow.held", {
1784
- transaction,
1785
- heldAmount,
1786
- reason
1787
- });
1788
- return this.plugins.executeHook("escrow.hold.after", this.getPluginContext(), {
1789
- transactionId,
1790
- ...options
1791
- }, async () => transaction);
1792
- });
1793
- }
1794
- /**
1795
- * Release funds from escrow to recipient
1796
- *
1797
- * @param transactionId - Transaction to release
1798
- * @param options - Release options
1799
- * @returns { transaction, releaseTransaction }
1800
- */
1801
- async release(transactionId, options) {
1802
- return this.plugins.executeHook("escrow.release.before", this.getPluginContext(), {
1803
- transactionId,
1804
- ...options
1805
- }, async () => {
1806
- const { amount = null, recipientId, recipientType = "organization", reason = RELEASE_REASON.PAYMENT_VERIFIED, releasedBy = null, createTransaction = true, metadata = {} } = options;
1807
- const TransactionModel = this.models.Transaction;
1808
- const transaction = await TransactionModel.findById(transactionId);
1809
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1810
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1811
- if (!recipientId) throw new ValidationError("recipientId is required for release", { transactionId });
1812
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
1813
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
1814
- if (releaseAmount > availableAmount) throw new ValidationError(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`, {
1815
- releaseAmount,
1816
- availableAmount,
1817
- transactionId
1818
- });
1819
- const releaseRecord = {
1820
- amount: releaseAmount,
1821
- recipientId,
1822
- recipientType,
1823
- releasedAt: /* @__PURE__ */ new Date(),
1824
- releasedBy,
1825
- reason,
1826
- metadata
1827
- };
1828
- transaction.hold.releases.push(releaseRecord);
1829
- transaction.hold.releasedAmount += releaseAmount;
1830
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
1831
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
1832
- if (isFullRelease) {
1833
- const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.RELEASED, transaction._id.toString(), {
1834
- changedBy: releasedBy ?? "system",
1835
- reason: `Escrow hold fully released: ${releaseAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
1836
- metadata: {
1837
- releaseAmount,
1838
- recipientId,
1839
- releaseReason: reason
1840
- }
1841
- });
1842
- transaction.hold.status = HOLD_STATUS.RELEASED;
1843
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
1844
- const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, TRANSACTION_STATUS.COMPLETED, transaction._id.toString(), {
1845
- changedBy: releasedBy ?? "system",
1846
- reason: `Transaction completed after full escrow release`,
1847
- metadata: {
1848
- releaseAmount,
1849
- recipientId
1850
- }
1851
- });
1852
- transaction.status = TRANSACTION_STATUS.COMPLETED;
1853
- Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
1854
- Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
1855
- } else if (isPartialRelease) {
1856
- const auditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.PARTIALLY_RELEASED, transaction._id.toString(), {
1857
- changedBy: releasedBy ?? "system",
1858
- reason: `Partial escrow release: ${releaseAmount} of ${transaction.hold.heldAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
1859
- metadata: {
1860
- releaseAmount,
1861
- recipientId,
1862
- releaseReason: reason,
1863
- remainingHeld: transaction.hold.heldAmount - transaction.hold.releasedAmount
1864
- }
1865
- });
1866
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
1867
- Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1868
- }
1869
- if ("markModified" in transaction) transaction.markModified("hold");
1870
- await transaction.save();
1871
- let releaseTaxAmount = 0;
1872
- if (transaction.tax && transaction.tax > 0) if (releaseAmount === availableAmount && !amount) {
1873
- const releasedTaxSoFar = transaction.hold.releasedTaxAmount ?? 0;
1874
- releaseTaxAmount = transaction.tax - releasedTaxSoFar;
1875
- } else {
1876
- const totalAmount = transaction.amount + transaction.tax;
1877
- if (totalAmount > 0) {
1878
- const taxRatio = transaction.tax / totalAmount;
1879
- releaseTaxAmount = Math.round(releaseAmount * taxRatio);
1880
- }
1881
- }
1882
- const releaseNetAmount = releaseAmount - releaseTaxAmount;
1883
- let releaseTransaction = null;
1884
- if (createTransaction) releaseTransaction = await TransactionModel.create({
1885
- organizationId: transaction.organizationId,
1886
- customerId: recipientId,
1887
- type: "escrow_release",
1888
- flow: "inflow",
1889
- tags: ["escrow", "release"],
1890
- amount: releaseAmount,
1891
- currency: transaction.currency,
1892
- fee: 0,
1893
- tax: releaseTaxAmount,
1894
- net: releaseNetAmount,
1895
- ...transaction.taxDetails && { taxDetails: transaction.taxDetails },
1896
- method: transaction.method,
1897
- status: "completed",
1898
- gateway: transaction.gateway,
1899
- sourceId: transaction._id,
1900
- sourceModel: "Transaction",
1901
- relatedTransactionId: transaction._id,
1902
- metadata: {
1903
- ...metadata,
1904
- isRelease: true,
1905
- heldTransactionId: transaction._id.toString(),
1906
- releaseReason: reason,
1907
- recipientType,
1908
- originalCategory: transaction.category
1909
- },
1910
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
1911
- });
1912
- this.events.emit("escrow.released", {
1913
- transaction,
1914
- releaseTransaction,
1915
- releaseAmount,
1916
- recipientId,
1917
- recipientType,
1918
- reason,
1919
- isFullRelease,
1920
- isPartialRelease
1921
- });
1922
- const result = {
1923
- transaction,
1924
- releaseTransaction,
1925
- releaseAmount,
1926
- isFullRelease,
1927
- isPartialRelease
1928
- };
1929
- return this.plugins.executeHook("escrow.release.after", this.getPluginContext(), {
1930
- transactionId,
1931
- ...options
1932
- }, async () => result);
1933
- });
1934
- }
1935
- /**
1936
- * Cancel hold and release back to customer
1937
- *
1938
- * @param transactionId - Transaction to cancel hold
1939
- * @param options - Cancel options
1940
- * @returns Updated transaction
1941
- */
1942
- async cancel(transactionId, options = {}) {
1943
- const { reason = "Hold cancelled", metadata = {} } = options;
1944
- const transaction = await this.models.Transaction.findById(transactionId);
1945
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1946
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1947
- const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.CANCELLED, transaction._id.toString(), {
1948
- changedBy: "system",
1949
- reason: `Escrow hold cancelled${reason ? ": " + reason : ""}`,
1950
- metadata: {
1951
- cancelReason: reason,
1952
- ...metadata
1953
- }
1954
- });
1955
- transaction.hold.status = HOLD_STATUS.CANCELLED;
1956
- transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
1957
- transaction.hold.metadata = {
1958
- ...transaction.hold.metadata,
1959
- ...metadata,
1960
- cancelReason: reason
1961
- };
1962
- const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, TRANSACTION_STATUS.CANCELLED, transaction._id.toString(), {
1963
- changedBy: "system",
1964
- reason: `Transaction cancelled due to escrow hold cancellation`,
1965
- metadata: { cancelReason: reason }
1966
- });
1967
- transaction.status = TRANSACTION_STATUS.CANCELLED;
1968
- Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
1969
- Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
1970
- if ("markModified" in transaction) transaction.markModified("hold");
1971
- await transaction.save();
1972
- this.events.emit("escrow.cancelled", {
1973
- transaction,
1974
- reason
1975
- });
1976
- return transaction;
1977
- }
1978
- /**
1979
- * Split payment to multiple recipients
1980
- * Deducts splits from held amount and releases remainder to organization
1981
- *
1982
- * @param transactionId - Transaction to split
1983
- * @param splitRules - Split configuration
1984
- * @returns { transaction, splitTransactions, organizationTransaction }
1985
- */
1986
- async split(transactionId, splitRules = []) {
1987
- const TransactionModel = this.models.Transaction;
1988
- const transaction = await TransactionModel.findById(transactionId);
1989
- if (!transaction) throw new TransactionNotFoundError(transactionId);
1990
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1991
- if (!splitRules || splitRules.length === 0) throw new ValidationError("splitRules cannot be empty", { transactionId });
1992
- const splits = calculateSplits(transaction.amount, splitRules, transaction.commission?.gatewayFeeRate ?? 0);
1993
- transaction.splits = splits;
1994
- await transaction.save();
1995
- const splitTransactions = [];
1996
- const totalTax = transaction.tax ?? 0;
1997
- const totalBaseAmount = transaction.amount;
1998
- let allocatedTaxAmount = 0;
1999
- const splitTaxAmounts = splits.map((split) => {
2000
- if (!totalTax || totalBaseAmount <= 0) return 0;
2001
- const ratio = split.grossAmount / totalBaseAmount;
2002
- const taxAmount = Math.round(totalTax * ratio);
2003
- allocatedTaxAmount += taxAmount;
2004
- return taxAmount;
2005
- });
2006
- for (const [index, split] of splits.entries()) {
2007
- const splitTaxAmount = totalTax > 0 ? splitTaxAmounts[index] ?? 0 : 0;
2008
- const splitNetAmount = split.grossAmount - split.gatewayFeeAmount - splitTaxAmount;
2009
- const splitTransaction = await TransactionModel.create({
2010
- organizationId: transaction.organizationId,
2011
- customerId: split.recipientId,
2012
- type: split.type,
2013
- flow: "outflow",
2014
- tags: ["split", "commission"],
2015
- amount: split.grossAmount,
2016
- currency: transaction.currency,
2017
- fee: split.gatewayFeeAmount,
2018
- tax: splitTaxAmount,
2019
- net: splitNetAmount,
2020
- ...transaction.taxDetails && splitTaxAmount > 0 && { taxDetails: transaction.taxDetails },
2021
- method: transaction.method,
2022
- status: "completed",
2023
- gateway: transaction.gateway,
2024
- sourceId: transaction._id,
2025
- sourceModel: "Transaction",
2026
- relatedTransactionId: transaction._id,
2027
- metadata: {
2028
- isSplit: true,
2029
- splitType: split.type,
2030
- recipientType: split.recipientType,
2031
- originalTransactionId: transaction._id.toString(),
2032
- splitGrossAmount: split.grossAmount,
2033
- splitNetAmount: split.netAmount,
2034
- gatewayFeeAmount: split.gatewayFeeAmount
2035
- },
2036
- idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
2037
- });
2038
- split.payoutTransactionId = splitTransaction._id.toString();
2039
- split.status = SPLIT_STATUS.PAID;
2040
- split.paidDate = /* @__PURE__ */ new Date();
2041
- splitTransactions.push(splitTransaction);
2042
- }
2043
- await transaction.save();
2044
- const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
2045
- const organizationTaxAmount = totalTax > 0 ? Math.max(0, totalTax - allocatedTaxAmount) : 0;
2046
- const organizationPayoutTotal = totalTax > 0 ? organizationPayout + organizationTaxAmount : organizationPayout;
2047
- const organizationTransaction = await this.release(transactionId, {
2048
- amount: organizationPayoutTotal,
2049
- recipientId: transaction.organizationId?.toString() ?? "",
2050
- recipientType: "organization",
2051
- reason: RELEASE_REASON.PAYMENT_VERIFIED,
2052
- createTransaction: true,
2053
- metadata: {
2054
- afterSplits: true,
2055
- totalSplits: splits.length,
2056
- totalSplitAmount: transaction.amount - organizationPayout
2057
- }
2058
- });
2059
- this.events.emit("escrow.split", {
2060
- transaction,
2061
- splits,
2062
- splitTransactions,
2063
- organizationTransaction: organizationTransaction.releaseTransaction,
2064
- organizationPayout
2065
- });
2066
- return {
2067
- transaction,
2068
- splits,
2069
- splitTransactions,
2070
- organizationTransaction: organizationTransaction.releaseTransaction,
2071
- organizationPayout
2072
- };
2073
- }
2074
- /**
2075
- * Get escrow status
2076
- *
2077
- * @param transactionId - Transaction ID
2078
- * @returns Escrow status
2079
- */
2080
- async getStatus(transactionId) {
2081
- const transaction = await this.models.Transaction.findById(transactionId);
2082
- if (!transaction) throw new TransactionNotFoundError(transactionId);
2083
- return {
2084
- transaction,
2085
- hold: transaction.hold ?? null,
2086
- splits: transaction.splits ?? [],
2087
- hasHold: !!transaction.hold,
2088
- hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2089
- };
2090
- }
2091
- };
2092
-
2093
- //#endregion
2094
- //#region src/application/services/settlement.service.ts
2095
- /**
2096
- * Settlement Service
2097
- *
2098
- * Handles payout tracking after splits are released from escrow:
2099
- *
2100
- * Flow:
2101
- * 1. Transaction has splits → 2. Escrow released → 3. Create settlements → 4. Process payouts → 5. Mark complete
2102
- *
2103
- * @example
2104
- * ```typescript
2105
- * // Auto-create settlements from transaction splits
2106
- * await revenue.settlement.createFromSplits(transactionId);
2107
- *
2108
- * // Schedule a manual payout
2109
- * await revenue.settlement.schedule({
2110
- * recipientId: vendorId,
2111
- * amount: 8500,
2112
- * payoutMethod: 'bank_transfer',
2113
- * });
2114
- *
2115
- * // Process pending payouts
2116
- * const result = await revenue.settlement.processPending({ limit: 100 });
2117
- *
2118
- * // Mark as completed after bank confirms
2119
- * await revenue.settlement.complete(settlementId, { transferReference: 'TRF123' });
2120
- * ```
2121
- */
2122
- /**
2123
- * Settlement Service
2124
- * Uses DI container for all dependencies
2125
- *
2126
- * Architecture:
2127
- * - PluginManager: Wraps operations with lifecycle hooks (before/after)
2128
- * - EventBus: Fire-and-forget notifications for completed operations
2129
- */
2130
- var SettlementService = class {
2131
- models;
2132
- plugins;
2133
- logger;
2134
- events;
2135
- constructor(container) {
2136
- this.models = container.get("models");
2137
- this.plugins = container.get("plugins");
2138
- this.logger = container.get("logger");
2139
- this.events = container.get("events");
2140
- this.plugins;
2141
- }
2142
- /**
2143
- * Create settlements from transaction splits
2144
- * Typically called after escrow is released
2145
- *
2146
- * @param transactionId - Transaction ID with splits
2147
- * @param options - Creation options
2148
- * @returns Array of created settlements
2149
- */
2150
- async createFromSplits(transactionId, options = {}) {
2151
- const { scheduledAt = /* @__PURE__ */ new Date(), payoutMethod = "bank_transfer", metadata = {} } = options;
2152
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2153
- const transaction = await this.models.Transaction.findById(transactionId);
2154
- if (!transaction) throw new TransactionNotFoundError(transactionId);
2155
- if (!transaction.splits || transaction.splits.length === 0) throw new ValidationError("Transaction has no splits to settle", { transactionId });
2156
- const SettlementModel = this.models.Settlement;
2157
- const settlements = [];
2158
- for (const split of transaction.splits) {
2159
- if (split.status === "paid") {
2160
- this.logger.info("Split already paid, skipping", { splitId: split._id });
2161
- continue;
2162
- }
2163
- const settlement = await SettlementModel.create({
2164
- organizationId: transaction.organizationId,
2165
- recipientId: split.recipientId,
2166
- recipientType: split.recipientType,
2167
- type: SETTLEMENT_TYPE.SPLIT_PAYOUT,
2168
- status: SETTLEMENT_STATUS.PENDING,
2169
- payoutMethod,
2170
- amount: split.netAmount,
2171
- currency: transaction.currency,
2172
- sourceTransactionIds: [transaction._id],
2173
- sourceSplitIds: [split._id?.toString() || ""],
2174
- scheduledAt,
2175
- metadata: {
2176
- ...metadata,
2177
- splitType: split.type,
2178
- transactionCategory: transaction.category
2179
- }
2180
- });
2181
- settlements.push(settlement);
2182
- }
2183
- this.events.emit("settlement.created", {
2184
- settlements,
2185
- transactionId,
2186
- count: settlements.length
2187
- });
2188
- this.logger.info("Created settlements from splits", {
2189
- transactionId,
2190
- count: settlements.length
2191
- });
2192
- return settlements;
2193
- }
2194
- /**
2195
- * Schedule a payout
2196
- *
2197
- * @param params - Settlement parameters
2198
- * @returns Created settlement
2199
- */
2200
- async schedule(params) {
2201
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2202
- const { organizationId, recipientId, recipientType, type, amount, currency = "USD", payoutMethod, sourceTransactionIds = [], sourceSplitIds = [], scheduledAt = /* @__PURE__ */ new Date(), bankTransferDetails, mobileWalletDetails, cryptoDetails, notes, metadata = {} } = params;
2203
- if (amount <= 0) throw new ValidationError("Settlement amount must be positive", { amount });
2204
- const settlement = await this.models.Settlement.create({
2205
- organizationId,
2206
- recipientId,
2207
- recipientType,
2208
- type,
2209
- status: SETTLEMENT_STATUS.PENDING,
2210
- payoutMethod,
2211
- amount,
2212
- currency,
2213
- sourceTransactionIds,
2214
- sourceSplitIds,
2215
- scheduledAt,
2216
- bankTransferDetails,
2217
- mobileWalletDetails,
2218
- cryptoDetails,
2219
- notes,
2220
- metadata
2221
- });
2222
- this.events.emit("settlement.scheduled", {
2223
- settlement,
2224
- scheduledAt
2225
- });
2226
- this.logger.info("Settlement scheduled", {
2227
- settlementId: settlement._id,
2228
- recipientId,
2229
- amount
2230
- });
2231
- return settlement;
2232
- }
2233
- /**
2234
- * Process pending settlements
2235
- * Batch process settlements that are due
2236
- *
2237
- * @param options - Processing options
2238
- * @returns Processing result
2239
- */
2240
- async processPending(options = {}) {
2241
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2242
- const { limit = 100, organizationId, payoutMethod, dryRun = false } = options;
2243
- const SettlementModel = this.models.Settlement;
2244
- const query = {
2245
- status: SETTLEMENT_STATUS.PENDING,
2246
- scheduledAt: { $lte: /* @__PURE__ */ new Date() }
2247
- };
2248
- if (organizationId) query.organizationId = organizationId;
2249
- if (payoutMethod) query.payoutMethod = payoutMethod;
2250
- const settlements = await SettlementModel.find(query).limit(limit).sort({ scheduledAt: 1 });
2251
- const result = {
2252
- processed: 0,
2253
- succeeded: 0,
2254
- failed: 0,
2255
- settlements: [],
2256
- errors: []
2257
- };
2258
- if (dryRun) {
2259
- this.logger.info("Dry run: would process settlements", { count: settlements.length });
2260
- result.settlements = settlements;
2261
- return result;
2262
- }
2263
- for (const settlement of settlements) {
2264
- result.processed++;
2265
- try {
2266
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.PROCESSING, settlement._id.toString(), {
2267
- changedBy: "system",
2268
- reason: "Settlement processing started",
2269
- metadata: {
2270
- recipientId: settlement.recipientId,
2271
- amount: settlement.amount
2272
- }
2273
- });
2274
- settlement.status = SETTLEMENT_STATUS.PROCESSING;
2275
- settlement.processedAt = /* @__PURE__ */ new Date();
2276
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2277
- await settlement.save();
2278
- result.succeeded++;
2279
- result.settlements.push(settlement);
2280
- this.events.emit("settlement.processing", {
2281
- settlement,
2282
- processedAt: settlement.processedAt
2283
- });
2284
- } catch (error) {
2285
- result.failed++;
2286
- result.errors.push({
2287
- settlementId: settlement._id.toString(),
2288
- error: error.message
2289
- });
2290
- this.logger.error("Failed to process settlement", {
2291
- settlementId: settlement._id,
2292
- error
2293
- });
2294
- }
2295
- }
2296
- this.logger.info("Processed settlements", result);
2297
- return result;
2298
- }
2299
- /**
2300
- * Mark settlement as completed
2301
- * Call this after bank confirms the transfer
2302
- *
2303
- * @param settlementId - Settlement ID
2304
- * @param details - Completion details
2305
- * @returns Updated settlement
2306
- */
2307
- async complete(settlementId, details = {}) {
2308
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2309
- const settlement = await this.models.Settlement.findById(settlementId);
2310
- if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2311
- if (settlement.status !== SETTLEMENT_STATUS.PROCESSING && settlement.status !== SETTLEMENT_STATUS.PENDING) throw new InvalidStateTransitionError("complete", SETTLEMENT_STATUS.PROCESSING, settlement.status, "Only processing or pending settlements can be completed");
2312
- const { transferReference, transferredAt = /* @__PURE__ */ new Date(), transactionHash, notes, metadata = {} } = details;
2313
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.COMPLETED, settlement._id.toString(), {
2314
- changedBy: "system",
2315
- reason: "Settlement completed successfully",
2316
- metadata: {
2317
- transferReference,
2318
- transferredAt,
2319
- transactionHash,
2320
- payoutMethod: settlement.payoutMethod,
2321
- amount: settlement.amount
2322
- }
2323
- });
2324
- settlement.status = SETTLEMENT_STATUS.COMPLETED;
2325
- settlement.completedAt = /* @__PURE__ */ new Date();
2326
- if (settlement.payoutMethod === "bank_transfer" && transferReference) settlement.bankTransferDetails = {
2327
- ...settlement.bankTransferDetails,
2328
- transferReference,
2329
- transferredAt
2330
- };
2331
- else if (settlement.payoutMethod === "crypto" && transactionHash) settlement.cryptoDetails = {
2332
- ...settlement.cryptoDetails,
2333
- transactionHash,
2334
- transferredAt
2335
- };
2336
- else if (settlement.payoutMethod === "mobile_wallet") settlement.mobileWalletDetails = {
2337
- ...settlement.mobileWalletDetails,
2338
- transferredAt
2339
- };
2340
- else if (settlement.payoutMethod === "platform_balance") settlement.platformBalanceDetails = {
2341
- ...settlement.platformBalanceDetails,
2342
- appliedAt: transferredAt
2343
- };
2344
- if (notes) settlement.notes = notes;
2345
- settlement.metadata = {
2346
- ...settlement.metadata,
2347
- ...metadata
2348
- };
2349
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2350
- await settlement.save();
2351
- this.events.emit("settlement.completed", {
2352
- settlement,
2353
- completedAt: settlement.completedAt
2354
- });
2355
- this.logger.info("Settlement completed", {
2356
- settlementId: settlement._id,
2357
- recipientId: settlement.recipientId,
2358
- amount: settlement.amount
2359
- });
2360
- return settlement;
2361
- }
2362
- /**
2363
- * Mark settlement as failed
2364
- *
2365
- * @param settlementId - Settlement ID
2366
- * @param reason - Failure reason
2367
- * @returns Updated settlement
2368
- */
2369
- async fail(settlementId, reason, options = {}) {
2370
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2371
- const settlement = await this.models.Settlement.findById(settlementId);
2372
- if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2373
- const { code, retry = false } = options;
2374
- if (retry) {
2375
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.PENDING, settlement._id.toString(), {
2376
- changedBy: "system",
2377
- reason: `Settlement failed, retrying: ${reason}`,
2378
- metadata: {
2379
- failureReason: reason,
2380
- failureCode: code,
2381
- retryCount: (settlement.retryCount || 0) + 1,
2382
- scheduledAt: new Date(Date.now() + 3600 * 1e3)
2383
- }
2384
- });
2385
- settlement.status = SETTLEMENT_STATUS.PENDING;
2386
- settlement.retryCount = (settlement.retryCount || 0) + 1;
2387
- settlement.scheduledAt = new Date(Date.now() + 3600 * 1e3);
2388
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2389
- } else {
2390
- const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.FAILED, settlement._id.toString(), {
2391
- changedBy: "system",
2392
- reason: `Settlement failed: ${reason}`,
2393
- metadata: {
2394
- failureReason: reason,
2395
- failureCode: code
2396
- }
2397
- });
2398
- settlement.status = SETTLEMENT_STATUS.FAILED;
2399
- settlement.failedAt = /* @__PURE__ */ new Date();
2400
- Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2401
- }
2402
- settlement.failureReason = reason;
2403
- if (code) settlement.failureCode = code;
2404
- await settlement.save();
2405
- this.events.emit("settlement.failed", {
2406
- settlement,
2407
- reason,
2408
- code,
2409
- retry
2410
- });
2411
- this.logger.warn("Settlement failed", {
2412
- settlementId: settlement._id,
2413
- reason,
2414
- retry
2415
- });
2416
- return settlement;
2417
- }
2418
- /**
2419
- * List settlements with filters
2420
- *
2421
- * @param filters - Query filters
2422
- * @returns Settlements
2423
- */
2424
- async list(filters = {}) {
2425
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2426
- const SettlementModel = this.models.Settlement;
2427
- const { organizationId, recipientId, status, type, payoutMethod, scheduledAfter, scheduledBefore, limit = 50, skip = 0, sort = { createdAt: -1 } } = filters;
2428
- const query = {};
2429
- if (organizationId) query.organizationId = organizationId;
2430
- if (recipientId) query.recipientId = recipientId;
2431
- if (status) query.status = Array.isArray(status) ? { $in: status } : status;
2432
- if (type) query.type = type;
2433
- if (payoutMethod) query.payoutMethod = payoutMethod;
2434
- if (scheduledAfter || scheduledBefore) {
2435
- query.scheduledAt = {};
2436
- if (scheduledAfter) query.scheduledAt.$gte = scheduledAfter;
2437
- if (scheduledBefore) query.scheduledAt.$lte = scheduledBefore;
2438
- }
2439
- return await SettlementModel.find(query).limit(limit).skip(skip).sort(sort);
2440
- }
2441
- /**
2442
- * Get payout summary for recipient
2443
- *
2444
- * @param recipientId - Recipient ID
2445
- * @param options - Summary options
2446
- * @returns Settlement summary
2447
- */
2448
- async getSummary(recipientId, options = {}) {
2449
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2450
- const { organizationId, startDate, endDate } = options;
2451
- const SettlementModel = this.models.Settlement;
2452
- const query = { recipientId };
2453
- if (organizationId) query.organizationId = organizationId;
2454
- if (startDate || endDate) {
2455
- query.createdAt = {};
2456
- if (startDate) query.createdAt.$gte = startDate;
2457
- if (endDate) query.createdAt.$lte = endDate;
2458
- }
2459
- const settlements = await SettlementModel.find(query);
2460
- const summary = {
2461
- recipientId,
2462
- totalPending: 0,
2463
- totalProcessing: 0,
2464
- totalCompleted: 0,
2465
- totalFailed: 0,
2466
- amountPending: 0,
2467
- amountCompleted: 0,
2468
- amountFailed: 0,
2469
- currency: settlements[0]?.currency || "USD",
2470
- settlements: {
2471
- pending: 0,
2472
- processing: 0,
2473
- completed: 0,
2474
- failed: 0,
2475
- cancelled: 0
2476
- }
2477
- };
2478
- for (const settlement of settlements) {
2479
- summary.settlements[settlement.status]++;
2480
- if (settlement.status === SETTLEMENT_STATUS.PENDING) {
2481
- summary.totalPending++;
2482
- summary.amountPending += settlement.amount;
2483
- } else if (settlement.status === SETTLEMENT_STATUS.PROCESSING) summary.totalProcessing++;
2484
- else if (settlement.status === SETTLEMENT_STATUS.COMPLETED) {
2485
- summary.totalCompleted++;
2486
- summary.amountCompleted += settlement.amount;
2487
- if (!summary.lastSettlementDate || settlement.completedAt > summary.lastSettlementDate) summary.lastSettlementDate = settlement.completedAt;
2488
- } else if (settlement.status === SETTLEMENT_STATUS.FAILED) {
2489
- summary.totalFailed++;
2490
- summary.amountFailed += settlement.amount;
2491
- }
2492
- }
2493
- return summary;
2494
- }
2495
- /**
2496
- * Get settlement by ID
2497
- *
2498
- * @param settlementId - Settlement ID
2499
- * @returns Settlement
2500
- */
2501
- async get(settlementId) {
2502
- if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2503
- const settlement = await this.models.Settlement.findById(settlementId);
2504
- if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2505
- return settlement;
2506
- }
2507
- };
2508
-
2509
- //#endregion
2510
- export { MonetizationService as a, getAuditTrail as c, SETTLEMENT_STATE_MACHINE as d, SPLIT_STATE_MACHINE as f, resolveConfig as g, StateMachine as h, PaymentService as i, getLastStateChange as l, TRANSACTION_STATE_MACHINE as m, EscrowService as n, appendAuditEvent as o, SUBSCRIPTION_STATE_MACHINE as p, TransactionService as r, filterAuditTrail as s, SettlementService as t, HOLD_STATE_MACHINE as u };