@classytic/revenue 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +8 -7
  2. package/dist/application/services/index.d.mts +4 -0
  3. package/dist/application/services/index.mjs +3 -0
  4. package/dist/base-CsTlVQJe.d.mts +136 -0
  5. package/dist/base-DCoyIUj6.mjs +152 -0
  6. package/dist/category-resolver-DV83N8ok.mjs +284 -0
  7. package/dist/commission-split-BzB8cd39.mjs +485 -0
  8. package/dist/core/events.d.mts +294 -0
  9. package/dist/core/events.mjs +100 -0
  10. package/dist/core/index.d.mts +9 -0
  11. package/dist/core/index.mjs +8 -0
  12. package/dist/enums/index.d.mts +157 -0
  13. package/dist/enums/index.mjs +56 -0
  14. package/dist/errors-rRdOqnWx.d.mts +787 -0
  15. package/dist/escrow.enums-CZGrrdg7.mjs +101 -0
  16. package/dist/{escrow.enums-CE0VQsfe.d.ts → escrow.enums-DwdLuuve.d.mts} +30 -28
  17. package/dist/idempotency-DaYcUGY1.mjs +172 -0
  18. package/dist/index-Dsp7H5Wb.d.mts +471 -0
  19. package/dist/index.d.mts +9 -0
  20. package/dist/index.mjs +38 -0
  21. package/dist/infrastructure/plugins/{index.d.ts → index.d.mts} +81 -109
  22. package/dist/infrastructure/plugins/index.mjs +345 -0
  23. package/dist/money-CvrDOijQ.mjs +271 -0
  24. package/dist/money-DPG8AtJ8.d.mts +112 -0
  25. package/dist/{payment.enums-C1BiGlRa.d.ts → payment.enums-HAuAS9Pp.d.mts} +14 -13
  26. package/dist/payment.enums-tEFVa-Xp.mjs +69 -0
  27. package/dist/plugin-BbK0OVHy.d.mts +327 -0
  28. package/dist/plugin-Cd_V04Em.mjs +210 -0
  29. package/dist/providers/index.d.mts +3 -0
  30. package/dist/providers/index.mjs +3 -0
  31. package/dist/reconciliation/{index.d.ts → index.d.mts} +90 -112
  32. package/dist/reconciliation/index.mjs +192 -0
  33. package/dist/retry-HHCOXYdn.d.mts +186 -0
  34. package/dist/revenue-BhdS7nXh.mjs +553 -0
  35. package/dist/schemas/index.d.mts +2665 -0
  36. package/dist/schemas/index.mjs +717 -0
  37. package/dist/schemas/validation.d.mts +375 -0
  38. package/dist/schemas/validation.mjs +325 -0
  39. package/dist/{settlement.enums-ByC1x0ye.d.ts → settlement.enums-DFhkqZEY.d.mts} +31 -29
  40. package/dist/settlement.schema-DnNSFpGd.d.mts +344 -0
  41. package/dist/settlement.service-DjzAjezU.d.mts +594 -0
  42. package/dist/settlement.service-DmdKv0Zu.mjs +2511 -0
  43. package/dist/split.enums-BrjabxIX.mjs +86 -0
  44. package/dist/split.enums-DmskfLOM.d.mts +43 -0
  45. package/dist/tax-BoCt5cEd.d.mts +61 -0
  46. package/dist/tax-EQ15DO81.mjs +162 -0
  47. package/dist/transaction.enums-pCyMFT4Z.mjs +96 -0
  48. package/dist/utils/{index.d.ts → index.d.mts} +91 -161
  49. package/dist/utils/index.mjs +346 -0
  50. package/package.json +39 -37
  51. package/dist/application/services/index.d.ts +0 -6
  52. package/dist/application/services/index.js +0 -3288
  53. package/dist/application/services/index.js.map +0 -1
  54. package/dist/core/events.d.ts +0 -455
  55. package/dist/core/events.js +0 -122
  56. package/dist/core/events.js.map +0 -1
  57. package/dist/core/index.d.ts +0 -13
  58. package/dist/core/index.js +0 -4591
  59. package/dist/core/index.js.map +0 -1
  60. package/dist/enums/index.d.ts +0 -159
  61. package/dist/enums/index.js +0 -296
  62. package/dist/enums/index.js.map +0 -1
  63. package/dist/index-DxIK0UmZ.d.ts +0 -633
  64. package/dist/index-EnfKzDbs.d.ts +0 -806
  65. package/dist/index-cLJBLUvx.d.ts +0 -478
  66. package/dist/index.d.ts +0 -43
  67. package/dist/index.js +0 -4864
  68. package/dist/index.js.map +0 -1
  69. package/dist/infrastructure/plugins/index.js +0 -292
  70. package/dist/infrastructure/plugins/index.js.map +0 -1
  71. package/dist/money-widWVD7r.d.ts +0 -111
  72. package/dist/plugin-Bb9HOE10.d.ts +0 -336
  73. package/dist/providers/index.d.ts +0 -145
  74. package/dist/providers/index.js +0 -141
  75. package/dist/providers/index.js.map +0 -1
  76. package/dist/reconciliation/index.js +0 -140
  77. package/dist/reconciliation/index.js.map +0 -1
  78. package/dist/retry-D4hFUwVk.d.ts +0 -194
  79. package/dist/schemas/index.d.ts +0 -2655
  80. package/dist/schemas/index.js +0 -841
  81. package/dist/schemas/index.js.map +0 -1
  82. package/dist/schemas/validation.d.ts +0 -384
  83. package/dist/schemas/validation.js +0 -303
  84. package/dist/schemas/validation.js.map +0 -1
  85. package/dist/settlement.schema-CpamV7ZY.d.ts +0 -343
  86. package/dist/split.enums-DG3TxQf9.d.ts +0 -42
  87. package/dist/tax-CV8A0sxl.d.ts +0 -60
  88. package/dist/utils/index.js +0 -1202
  89. package/dist/utils/index.js.map +0 -1
@@ -0,0 +1,2511 @@
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
+ type: transaction.gateway?.type ?? gatewayType,
1387
+ provider: transaction.gateway?.provider ?? "manual",
1388
+ paymentIntentId: refundResult.id,
1389
+ chargeId: refundResult.id
1390
+ },
1391
+ paymentDetails: transaction.paymentDetails,
1392
+ ...refundCommission && { commission: refundCommission },
1393
+ ...transaction.sourceId && { sourceId: transaction.sourceId },
1394
+ ...transaction.sourceModel && { sourceModel: transaction.sourceModel },
1395
+ relatedTransactionId: transaction._id,
1396
+ metadata: {
1397
+ ...transaction.metadata,
1398
+ isRefund: true,
1399
+ originalTransactionId: transaction._id.toString(),
1400
+ refundReason: reason,
1401
+ refundResult: refundResult.metadata
1402
+ },
1403
+ idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1404
+ });
1405
+ const isPartialRefund = refundAmount < refundableAmount;
1406
+ const refundStatus = isPartialRefund ? "partially_refunded" : "refunded";
1407
+ const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, refundStatus, transaction._id.toString(), {
1408
+ changedBy: "system",
1409
+ reason: `Refund processed: ${isPartialRefund ? "partial" : "full"} refund of ${refundAmount}${reason ? " - " + reason : ""}`,
1410
+ metadata: {
1411
+ refundAmount,
1412
+ isPartialRefund,
1413
+ refundTransactionId: refundTransaction._id.toString()
1414
+ }
1415
+ });
1416
+ transaction.status = refundStatus;
1417
+ transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1418
+ transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1419
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1420
+ transaction.metadata = {
1421
+ ...transaction.metadata,
1422
+ refundTransactionId: refundTransaction._id.toString(),
1423
+ refundReason: reason
1424
+ };
1425
+ await transaction.save();
1426
+ this.events.emit("payment.refunded", {
1427
+ transaction,
1428
+ refundTransaction,
1429
+ refundResult: {
1430
+ ...refundResult,
1431
+ currency: refundResult.currency ?? "USD",
1432
+ metadata: refundResult.metadata ?? {}
1433
+ },
1434
+ refundAmount,
1435
+ reason: reason ?? void 0,
1436
+ isPartialRefund
1437
+ });
1438
+ const result = {
1439
+ transaction,
1440
+ refundTransaction,
1441
+ refundResult,
1442
+ status: transaction.status
1443
+ };
1444
+ return this.plugins.executeHook("payment.refund.after", this.getPluginContext(), {
1445
+ transactionId: paymentId,
1446
+ amount,
1447
+ ...options
1448
+ }, async () => result);
1449
+ });
1450
+ }
1451
+ /**
1452
+ * Handle webhook from payment provider
1453
+ *
1454
+ * @param provider - Provider name
1455
+ * @param payload - Webhook payload
1456
+ * @param headers - Request headers
1457
+ * @returns { event, transaction }
1458
+ */
1459
+ async handleWebhook(providerName, payload, headers = {}) {
1460
+ const provider = this.providers[providerName];
1461
+ if (!provider) throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1462
+ if (!provider.getCapabilities().supportsWebhooks) throw new ProviderCapabilityError(providerName, "webhooks");
1463
+ let webhookEvent;
1464
+ try {
1465
+ webhookEvent = await this.executeProviderCall(() => provider.handleWebhook(payload, headers), `${providerName}.handleWebhook`);
1466
+ } catch (error) {
1467
+ this.logger.error("Webhook processing failed:", error);
1468
+ throw new ProviderError(`Webhook processing failed for ${providerName}: ${error.message}`, "WEBHOOK_PROCESSING_FAILED", { retryable: false });
1469
+ }
1470
+ if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) throw new ValidationError(`Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`, {
1471
+ provider: providerName,
1472
+ eventType: webhookEvent?.type
1473
+ });
1474
+ const TransactionModel = this.models.Transaction;
1475
+ let transaction = null;
1476
+ if (webhookEvent.data.sessionId) transaction = await TransactionModel.findOne({ "gateway.sessionId": webhookEvent.data.sessionId });
1477
+ if (!transaction && webhookEvent.data.paymentIntentId) transaction = await TransactionModel.findOne({ "gateway.paymentIntentId": webhookEvent.data.paymentIntentId });
1478
+ if (!transaction) {
1479
+ this.logger.warn("Transaction not found for webhook event", {
1480
+ provider: providerName,
1481
+ eventId: webhookEvent.id,
1482
+ sessionId: webhookEvent.data.sessionId,
1483
+ paymentIntentId: webhookEvent.data.paymentIntentId
1484
+ });
1485
+ throw new TransactionNotFoundError(webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown");
1486
+ }
1487
+ if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) transaction.gateway = {
1488
+ ...transaction.gateway,
1489
+ type: transaction.gateway?.type ?? "manual",
1490
+ sessionId: webhookEvent.data.sessionId
1491
+ };
1492
+ if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) transaction.gateway = {
1493
+ ...transaction.gateway,
1494
+ type: transaction.gateway?.type ?? "manual",
1495
+ paymentIntentId: webhookEvent.data.paymentIntentId
1496
+ };
1497
+ if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
1498
+ this.logger.warn("Webhook already processed", {
1499
+ transactionId: transaction._id,
1500
+ eventId: webhookEvent.id
1501
+ });
1502
+ return {
1503
+ event: webhookEvent,
1504
+ transaction,
1505
+ status: "already_processed"
1506
+ };
1507
+ }
1508
+ transaction.webhook = {
1509
+ eventId: webhookEvent.id,
1510
+ eventType: webhookEvent.type,
1511
+ receivedAt: /* @__PURE__ */ new Date(),
1512
+ processedAt: /* @__PURE__ */ new Date(),
1513
+ data: webhookEvent.data
1514
+ };
1515
+ let newStatus = transaction.status;
1516
+ if (webhookEvent.type === "payment.succeeded") newStatus = "verified";
1517
+ else if (webhookEvent.type === "payment.failed") newStatus = "failed";
1518
+ else if (webhookEvent.type === "refund.succeeded") newStatus = "refunded";
1519
+ if (newStatus !== transaction.status) {
1520
+ const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, newStatus, transaction._id.toString(), {
1521
+ changedBy: "webhook",
1522
+ reason: `Webhook event: ${webhookEvent.type}`,
1523
+ metadata: {
1524
+ webhookId: webhookEvent.id,
1525
+ webhookType: webhookEvent.type,
1526
+ webhookData: webhookEvent.data
1527
+ }
1528
+ });
1529
+ transaction.status = newStatus;
1530
+ if (newStatus === "verified") transaction.verifiedAt = webhookEvent.createdAt;
1531
+ else if (newStatus === "refunded") transaction.refundedAt = webhookEvent.createdAt;
1532
+ else if (newStatus === "failed") transaction.failedAt = webhookEvent.createdAt;
1533
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1534
+ }
1535
+ await transaction.save();
1536
+ this.events.emit("webhook.processed", {
1537
+ webhookType: webhookEvent.type,
1538
+ provider: webhookEvent.provider,
1539
+ event: webhookEvent,
1540
+ transaction,
1541
+ processedAt: /* @__PURE__ */ new Date()
1542
+ });
1543
+ return {
1544
+ event: webhookEvent,
1545
+ transaction,
1546
+ status: "processed"
1547
+ };
1548
+ }
1549
+ /**
1550
+ * List payments/transactions with filters
1551
+ *
1552
+ * @param filters - Query filters
1553
+ * @param options - Query options (limit, skip, sort)
1554
+ * @returns Transactions
1555
+ */
1556
+ async list(filters = {}, options = {}) {
1557
+ const TransactionModel = this.models.Transaction;
1558
+ const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1559
+ return await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
1560
+ }
1561
+ /**
1562
+ * Get payment/transaction by ID
1563
+ *
1564
+ * @param transactionId - Transaction ID
1565
+ * @returns Transaction
1566
+ */
1567
+ async get(transactionId) {
1568
+ const transaction = await this.models.Transaction.findById(transactionId);
1569
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1570
+ return transaction;
1571
+ }
1572
+ /**
1573
+ * Get provider instance
1574
+ *
1575
+ * @param providerName - Provider name
1576
+ * @returns Provider instance
1577
+ */
1578
+ getProvider(providerName) {
1579
+ const provider = this.providers[providerName];
1580
+ if (!provider) throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1581
+ return provider;
1582
+ }
1583
+ /**
1584
+ * Find transaction by sessionId, paymentIntentId, or transaction ID
1585
+ * @private
1586
+ */
1587
+ async _findTransaction(TransactionModel, identifier) {
1588
+ let transaction = await TransactionModel.findOne({ "gateway.sessionId": identifier });
1589
+ if (!transaction) transaction = await TransactionModel.findOne({ "gateway.paymentIntentId": identifier });
1590
+ if (!transaction) transaction = await TransactionModel.findById(identifier);
1591
+ return transaction;
1592
+ }
1593
+ };
1594
+
1595
+ //#endregion
1596
+ //#region src/application/services/transaction.service.ts
1597
+ /**
1598
+ * Transaction Service
1599
+ * @classytic/revenue
1600
+ *
1601
+ * Thin, focused transaction service for core operations
1602
+ * Users handle their own analytics, exports, and complex queries
1603
+ *
1604
+ * Works with ANY model implementation:
1605
+ * - Plain Mongoose models
1606
+ * - @classytic/mongokit Repository instances
1607
+ * - Any other abstraction with compatible interface
1608
+ */
1609
+ /**
1610
+ * Transaction Service
1611
+ * Focused on core transaction lifecycle operations
1612
+ *
1613
+ * Architecture:
1614
+ * - PluginManager: Wraps operations with lifecycle hooks (before/after)
1615
+ * - EventBus: Fire-and-forget notifications for completed operations
1616
+ */
1617
+ var TransactionService = class {
1618
+ models;
1619
+ plugins;
1620
+ events;
1621
+ logger;
1622
+ constructor(container) {
1623
+ this.models = container.get("models");
1624
+ this.plugins = container.get("plugins");
1625
+ this.events = container.get("events");
1626
+ this.logger = container.get("logger");
1627
+ }
1628
+ /**
1629
+ * Create plugin context for hook execution
1630
+ * @private
1631
+ */
1632
+ getPluginContext() {
1633
+ return {
1634
+ events: this.events,
1635
+ logger: this.logger,
1636
+ storage: /* @__PURE__ */ new Map(),
1637
+ meta: {
1638
+ requestId: nanoid(),
1639
+ timestamp: /* @__PURE__ */ new Date()
1640
+ }
1641
+ };
1642
+ }
1643
+ /**
1644
+ * Get transaction by ID
1645
+ *
1646
+ * @param transactionId - Transaction ID
1647
+ * @returns Transaction
1648
+ */
1649
+ async get(transactionId) {
1650
+ const transaction = await this.models.Transaction.findById(transactionId);
1651
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1652
+ return transaction;
1653
+ }
1654
+ /**
1655
+ * List transactions with filters
1656
+ *
1657
+ * @param filters - Query filters
1658
+ * @param options - Query options (limit, skip, sort, populate)
1659
+ * @returns { transactions, total, page, limit }
1660
+ */
1661
+ async list(filters = {}, options = {}) {
1662
+ const TransactionModel = this.models.Transaction;
1663
+ const { limit = 50, skip = 0, page = null, sort = { createdAt: -1 }, populate = [] } = options;
1664
+ const actualSkip = page ? (page - 1) * limit : skip;
1665
+ let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
1666
+ if (populate.length > 0 && typeof query.populate === "function") populate.forEach((field) => {
1667
+ query = query.populate(field);
1668
+ });
1669
+ const transactions = await query;
1670
+ const model = TransactionModel;
1671
+ const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
1672
+ return {
1673
+ transactions,
1674
+ total,
1675
+ page: page ?? Math.floor(actualSkip / limit) + 1,
1676
+ limit,
1677
+ pages: Math.ceil(total / limit)
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Update transaction
1682
+ *
1683
+ * @param transactionId - Transaction ID
1684
+ * @param updates - Fields to update
1685
+ * @returns Updated transaction
1686
+ */
1687
+ async update(transactionId, updates) {
1688
+ const hookInput = {
1689
+ transactionId,
1690
+ updates
1691
+ };
1692
+ return this.plugins.executeHook("transaction.update.before", this.getPluginContext(), hookInput, async () => {
1693
+ const TransactionModel = this.models.Transaction;
1694
+ const effectiveUpdates = hookInput.updates;
1695
+ const model = TransactionModel;
1696
+ let transaction;
1697
+ if (typeof model.update === "function") transaction = await model.update(transactionId, effectiveUpdates);
1698
+ else if (typeof model.findByIdAndUpdate === "function") transaction = await model.findByIdAndUpdate(transactionId, { $set: effectiveUpdates }, { new: true });
1699
+ else throw new Error("Transaction model does not support update operations");
1700
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1701
+ this.events.emit("transaction.updated", {
1702
+ transaction,
1703
+ updates: effectiveUpdates
1704
+ });
1705
+ return this.plugins.executeHook("transaction.update.after", this.getPluginContext(), {
1706
+ transactionId,
1707
+ updates: effectiveUpdates
1708
+ }, async () => transaction);
1709
+ });
1710
+ }
1711
+ };
1712
+
1713
+ //#endregion
1714
+ //#region src/application/services/escrow.service.ts
1715
+ /**
1716
+ * Escrow Service
1717
+ * @classytic/revenue
1718
+ *
1719
+ * Platform-as-intermediary payment flow
1720
+ * Hold funds → Verify → Split/Deduct → Release to organization
1721
+ */
1722
+ /**
1723
+ * Escrow Service
1724
+ * Uses DI container for all dependencies
1725
+ *
1726
+ * Architecture:
1727
+ * - PluginManager: Wraps operations with lifecycle hooks (before/after)
1728
+ * - EventBus: Fire-and-forget notifications for completed operations
1729
+ */
1730
+ var EscrowService = class {
1731
+ models;
1732
+ plugins;
1733
+ logger;
1734
+ events;
1735
+ constructor(container) {
1736
+ this.models = container.get("models");
1737
+ this.plugins = container.get("plugins");
1738
+ this.logger = container.get("logger");
1739
+ this.events = container.get("events");
1740
+ }
1741
+ /**
1742
+ * Create plugin context for hook execution
1743
+ * @private
1744
+ */
1745
+ getPluginContext() {
1746
+ return {
1747
+ events: this.events,
1748
+ logger: this.logger,
1749
+ storage: /* @__PURE__ */ new Map(),
1750
+ meta: {
1751
+ requestId: nanoid(),
1752
+ timestamp: /* @__PURE__ */ new Date()
1753
+ }
1754
+ };
1755
+ }
1756
+ /**
1757
+ * Hold funds in escrow
1758
+ *
1759
+ * @param transactionId - Transaction to hold
1760
+ * @param options - Hold options
1761
+ * @returns Updated transaction
1762
+ */
1763
+ async hold(transactionId, options = {}) {
1764
+ return this.plugins.executeHook("escrow.hold.before", this.getPluginContext(), {
1765
+ transactionId,
1766
+ ...options
1767
+ }, async () => {
1768
+ const { reason = HOLD_REASON.PAYMENT_VERIFICATION, holdUntil = null, metadata = {} } = options;
1769
+ const transaction = await this.models.Transaction.findById(transactionId);
1770
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1771
+ if (transaction.status !== TRANSACTION_STATUS.VERIFIED) throw new InvalidStateTransitionError("transaction", transaction._id.toString(), transaction.status, TRANSACTION_STATUS.VERIFIED);
1772
+ const heldAmount = transaction.amount;
1773
+ transaction.hold = {
1774
+ status: HOLD_STATUS.HELD,
1775
+ heldAmount,
1776
+ releasedAmount: 0,
1777
+ reason,
1778
+ heldAt: /* @__PURE__ */ new Date(),
1779
+ ...holdUntil && { holdUntil },
1780
+ releases: [],
1781
+ metadata
1782
+ };
1783
+ await transaction.save();
1784
+ this.events.emit("escrow.held", {
1785
+ transaction,
1786
+ heldAmount,
1787
+ reason
1788
+ });
1789
+ return this.plugins.executeHook("escrow.hold.after", this.getPluginContext(), {
1790
+ transactionId,
1791
+ ...options
1792
+ }, async () => transaction);
1793
+ });
1794
+ }
1795
+ /**
1796
+ * Release funds from escrow to recipient
1797
+ *
1798
+ * @param transactionId - Transaction to release
1799
+ * @param options - Release options
1800
+ * @returns { transaction, releaseTransaction }
1801
+ */
1802
+ async release(transactionId, options) {
1803
+ return this.plugins.executeHook("escrow.release.before", this.getPluginContext(), {
1804
+ transactionId,
1805
+ ...options
1806
+ }, async () => {
1807
+ const { amount = null, recipientId, recipientType = "organization", reason = RELEASE_REASON.PAYMENT_VERIFIED, releasedBy = null, createTransaction = true, metadata = {} } = options;
1808
+ const TransactionModel = this.models.Transaction;
1809
+ const transaction = await TransactionModel.findById(transactionId);
1810
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1811
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1812
+ if (!recipientId) throw new ValidationError("recipientId is required for release", { transactionId });
1813
+ const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
1814
+ const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
1815
+ if (releaseAmount > availableAmount) throw new ValidationError(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`, {
1816
+ releaseAmount,
1817
+ availableAmount,
1818
+ transactionId
1819
+ });
1820
+ const releaseRecord = {
1821
+ amount: releaseAmount,
1822
+ recipientId,
1823
+ recipientType,
1824
+ releasedAt: /* @__PURE__ */ new Date(),
1825
+ releasedBy,
1826
+ reason,
1827
+ metadata
1828
+ };
1829
+ transaction.hold.releases.push(releaseRecord);
1830
+ transaction.hold.releasedAmount += releaseAmount;
1831
+ const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
1832
+ const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
1833
+ if (isFullRelease) {
1834
+ const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.RELEASED, transaction._id.toString(), {
1835
+ changedBy: releasedBy ?? "system",
1836
+ reason: `Escrow hold fully released: ${releaseAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
1837
+ metadata: {
1838
+ releaseAmount,
1839
+ recipientId,
1840
+ releaseReason: reason
1841
+ }
1842
+ });
1843
+ transaction.hold.status = HOLD_STATUS.RELEASED;
1844
+ transaction.hold.releasedAt = /* @__PURE__ */ new Date();
1845
+ const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, TRANSACTION_STATUS.COMPLETED, transaction._id.toString(), {
1846
+ changedBy: releasedBy ?? "system",
1847
+ reason: `Transaction completed after full escrow release`,
1848
+ metadata: {
1849
+ releaseAmount,
1850
+ recipientId
1851
+ }
1852
+ });
1853
+ transaction.status = TRANSACTION_STATUS.COMPLETED;
1854
+ Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
1855
+ Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
1856
+ } else if (isPartialRelease) {
1857
+ const auditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.PARTIALLY_RELEASED, transaction._id.toString(), {
1858
+ changedBy: releasedBy ?? "system",
1859
+ reason: `Partial escrow release: ${releaseAmount} of ${transaction.hold.heldAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
1860
+ metadata: {
1861
+ releaseAmount,
1862
+ recipientId,
1863
+ releaseReason: reason,
1864
+ remainingHeld: transaction.hold.heldAmount - transaction.hold.releasedAmount
1865
+ }
1866
+ });
1867
+ transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
1868
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1869
+ }
1870
+ if ("markModified" in transaction) transaction.markModified("hold");
1871
+ await transaction.save();
1872
+ let releaseTaxAmount = 0;
1873
+ if (transaction.tax && transaction.tax > 0) if (releaseAmount === availableAmount && !amount) {
1874
+ const releasedTaxSoFar = transaction.hold.releasedTaxAmount ?? 0;
1875
+ releaseTaxAmount = transaction.tax - releasedTaxSoFar;
1876
+ } else {
1877
+ const totalAmount = transaction.amount + transaction.tax;
1878
+ if (totalAmount > 0) {
1879
+ const taxRatio = transaction.tax / totalAmount;
1880
+ releaseTaxAmount = Math.round(releaseAmount * taxRatio);
1881
+ }
1882
+ }
1883
+ const releaseNetAmount = releaseAmount - releaseTaxAmount;
1884
+ let releaseTransaction = null;
1885
+ if (createTransaction) releaseTransaction = await TransactionModel.create({
1886
+ organizationId: transaction.organizationId,
1887
+ customerId: recipientId,
1888
+ type: "escrow_release",
1889
+ flow: "inflow",
1890
+ tags: ["escrow", "release"],
1891
+ amount: releaseAmount,
1892
+ currency: transaction.currency,
1893
+ fee: 0,
1894
+ tax: releaseTaxAmount,
1895
+ net: releaseNetAmount,
1896
+ ...transaction.taxDetails && { taxDetails: transaction.taxDetails },
1897
+ method: transaction.method,
1898
+ status: "completed",
1899
+ gateway: transaction.gateway,
1900
+ sourceId: transaction._id,
1901
+ sourceModel: "Transaction",
1902
+ relatedTransactionId: transaction._id,
1903
+ metadata: {
1904
+ ...metadata,
1905
+ isRelease: true,
1906
+ heldTransactionId: transaction._id.toString(),
1907
+ releaseReason: reason,
1908
+ recipientType,
1909
+ originalCategory: transaction.category
1910
+ },
1911
+ idempotencyKey: `release_${transaction._id}_${Date.now()}`
1912
+ });
1913
+ this.events.emit("escrow.released", {
1914
+ transaction,
1915
+ releaseTransaction,
1916
+ releaseAmount,
1917
+ recipientId,
1918
+ recipientType,
1919
+ reason,
1920
+ isFullRelease,
1921
+ isPartialRelease
1922
+ });
1923
+ const result = {
1924
+ transaction,
1925
+ releaseTransaction,
1926
+ releaseAmount,
1927
+ isFullRelease,
1928
+ isPartialRelease
1929
+ };
1930
+ return this.plugins.executeHook("escrow.release.after", this.getPluginContext(), {
1931
+ transactionId,
1932
+ ...options
1933
+ }, async () => result);
1934
+ });
1935
+ }
1936
+ /**
1937
+ * Cancel hold and release back to customer
1938
+ *
1939
+ * @param transactionId - Transaction to cancel hold
1940
+ * @param options - Cancel options
1941
+ * @returns Updated transaction
1942
+ */
1943
+ async cancel(transactionId, options = {}) {
1944
+ const { reason = "Hold cancelled", metadata = {} } = options;
1945
+ const transaction = await this.models.Transaction.findById(transactionId);
1946
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1947
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1948
+ const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(transaction.hold.status, HOLD_STATUS.CANCELLED, transaction._id.toString(), {
1949
+ changedBy: "system",
1950
+ reason: `Escrow hold cancelled${reason ? ": " + reason : ""}`,
1951
+ metadata: {
1952
+ cancelReason: reason,
1953
+ ...metadata
1954
+ }
1955
+ });
1956
+ transaction.hold.status = HOLD_STATUS.CANCELLED;
1957
+ transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
1958
+ transaction.hold.metadata = {
1959
+ ...transaction.hold.metadata,
1960
+ ...metadata,
1961
+ cancelReason: reason
1962
+ };
1963
+ const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(transaction.status, TRANSACTION_STATUS.CANCELLED, transaction._id.toString(), {
1964
+ changedBy: "system",
1965
+ reason: `Transaction cancelled due to escrow hold cancellation`,
1966
+ metadata: { cancelReason: reason }
1967
+ });
1968
+ transaction.status = TRANSACTION_STATUS.CANCELLED;
1969
+ Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
1970
+ Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
1971
+ if ("markModified" in transaction) transaction.markModified("hold");
1972
+ await transaction.save();
1973
+ this.events.emit("escrow.cancelled", {
1974
+ transaction,
1975
+ reason
1976
+ });
1977
+ return transaction;
1978
+ }
1979
+ /**
1980
+ * Split payment to multiple recipients
1981
+ * Deducts splits from held amount and releases remainder to organization
1982
+ *
1983
+ * @param transactionId - Transaction to split
1984
+ * @param splitRules - Split configuration
1985
+ * @returns { transaction, splitTransactions, organizationTransaction }
1986
+ */
1987
+ async split(transactionId, splitRules = []) {
1988
+ const TransactionModel = this.models.Transaction;
1989
+ const transaction = await TransactionModel.findById(transactionId);
1990
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
1991
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) throw new InvalidStateTransitionError("escrow_hold", transaction._id.toString(), transaction.hold?.status ?? "none", HOLD_STATUS.HELD);
1992
+ if (!splitRules || splitRules.length === 0) throw new ValidationError("splitRules cannot be empty", { transactionId });
1993
+ const splits = calculateSplits(transaction.amount, splitRules, transaction.commission?.gatewayFeeRate ?? 0);
1994
+ transaction.splits = splits;
1995
+ await transaction.save();
1996
+ const splitTransactions = [];
1997
+ const totalTax = transaction.tax ?? 0;
1998
+ const totalBaseAmount = transaction.amount;
1999
+ let allocatedTaxAmount = 0;
2000
+ const splitTaxAmounts = splits.map((split) => {
2001
+ if (!totalTax || totalBaseAmount <= 0) return 0;
2002
+ const ratio = split.grossAmount / totalBaseAmount;
2003
+ const taxAmount = Math.round(totalTax * ratio);
2004
+ allocatedTaxAmount += taxAmount;
2005
+ return taxAmount;
2006
+ });
2007
+ for (const [index, split] of splits.entries()) {
2008
+ const splitTaxAmount = totalTax > 0 ? splitTaxAmounts[index] ?? 0 : 0;
2009
+ const splitNetAmount = split.grossAmount - split.gatewayFeeAmount - splitTaxAmount;
2010
+ const splitTransaction = await TransactionModel.create({
2011
+ organizationId: transaction.organizationId,
2012
+ customerId: split.recipientId,
2013
+ type: split.type,
2014
+ flow: "outflow",
2015
+ tags: ["split", "commission"],
2016
+ amount: split.grossAmount,
2017
+ currency: transaction.currency,
2018
+ fee: split.gatewayFeeAmount,
2019
+ tax: splitTaxAmount,
2020
+ net: splitNetAmount,
2021
+ ...transaction.taxDetails && splitTaxAmount > 0 && { taxDetails: transaction.taxDetails },
2022
+ method: transaction.method,
2023
+ status: "completed",
2024
+ gateway: transaction.gateway,
2025
+ sourceId: transaction._id,
2026
+ sourceModel: "Transaction",
2027
+ relatedTransactionId: transaction._id,
2028
+ metadata: {
2029
+ isSplit: true,
2030
+ splitType: split.type,
2031
+ recipientType: split.recipientType,
2032
+ originalTransactionId: transaction._id.toString(),
2033
+ splitGrossAmount: split.grossAmount,
2034
+ splitNetAmount: split.netAmount,
2035
+ gatewayFeeAmount: split.gatewayFeeAmount
2036
+ },
2037
+ idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
2038
+ });
2039
+ split.payoutTransactionId = splitTransaction._id.toString();
2040
+ split.status = SPLIT_STATUS.PAID;
2041
+ split.paidDate = /* @__PURE__ */ new Date();
2042
+ splitTransactions.push(splitTransaction);
2043
+ }
2044
+ await transaction.save();
2045
+ const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
2046
+ const organizationTaxAmount = totalTax > 0 ? Math.max(0, totalTax - allocatedTaxAmount) : 0;
2047
+ const organizationPayoutTotal = totalTax > 0 ? organizationPayout + organizationTaxAmount : organizationPayout;
2048
+ const organizationTransaction = await this.release(transactionId, {
2049
+ amount: organizationPayoutTotal,
2050
+ recipientId: transaction.organizationId?.toString() ?? "",
2051
+ recipientType: "organization",
2052
+ reason: RELEASE_REASON.PAYMENT_VERIFIED,
2053
+ createTransaction: true,
2054
+ metadata: {
2055
+ afterSplits: true,
2056
+ totalSplits: splits.length,
2057
+ totalSplitAmount: transaction.amount - organizationPayout
2058
+ }
2059
+ });
2060
+ this.events.emit("escrow.split", {
2061
+ transaction,
2062
+ splits,
2063
+ splitTransactions,
2064
+ organizationTransaction: organizationTransaction.releaseTransaction,
2065
+ organizationPayout
2066
+ });
2067
+ return {
2068
+ transaction,
2069
+ splits,
2070
+ splitTransactions,
2071
+ organizationTransaction: organizationTransaction.releaseTransaction,
2072
+ organizationPayout
2073
+ };
2074
+ }
2075
+ /**
2076
+ * Get escrow status
2077
+ *
2078
+ * @param transactionId - Transaction ID
2079
+ * @returns Escrow status
2080
+ */
2081
+ async getStatus(transactionId) {
2082
+ const transaction = await this.models.Transaction.findById(transactionId);
2083
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
2084
+ return {
2085
+ transaction,
2086
+ hold: transaction.hold ?? null,
2087
+ splits: transaction.splits ?? [],
2088
+ hasHold: !!transaction.hold,
2089
+ hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2090
+ };
2091
+ }
2092
+ };
2093
+
2094
+ //#endregion
2095
+ //#region src/application/services/settlement.service.ts
2096
+ /**
2097
+ * Settlement Service
2098
+ *
2099
+ * Handles payout tracking after splits are released from escrow:
2100
+ *
2101
+ * Flow:
2102
+ * 1. Transaction has splits → 2. Escrow released → 3. Create settlements → 4. Process payouts → 5. Mark complete
2103
+ *
2104
+ * @example
2105
+ * ```typescript
2106
+ * // Auto-create settlements from transaction splits
2107
+ * await revenue.settlement.createFromSplits(transactionId);
2108
+ *
2109
+ * // Schedule a manual payout
2110
+ * await revenue.settlement.schedule({
2111
+ * recipientId: vendorId,
2112
+ * amount: 8500,
2113
+ * payoutMethod: 'bank_transfer',
2114
+ * });
2115
+ *
2116
+ * // Process pending payouts
2117
+ * const result = await revenue.settlement.processPending({ limit: 100 });
2118
+ *
2119
+ * // Mark as completed after bank confirms
2120
+ * await revenue.settlement.complete(settlementId, { transferReference: 'TRF123' });
2121
+ * ```
2122
+ */
2123
+ /**
2124
+ * Settlement Service
2125
+ * Uses DI container for all dependencies
2126
+ *
2127
+ * Architecture:
2128
+ * - PluginManager: Wraps operations with lifecycle hooks (before/after)
2129
+ * - EventBus: Fire-and-forget notifications for completed operations
2130
+ */
2131
+ var SettlementService = class {
2132
+ models;
2133
+ plugins;
2134
+ logger;
2135
+ events;
2136
+ constructor(container) {
2137
+ this.models = container.get("models");
2138
+ this.plugins = container.get("plugins");
2139
+ this.logger = container.get("logger");
2140
+ this.events = container.get("events");
2141
+ this.plugins;
2142
+ }
2143
+ /**
2144
+ * Create settlements from transaction splits
2145
+ * Typically called after escrow is released
2146
+ *
2147
+ * @param transactionId - Transaction ID with splits
2148
+ * @param options - Creation options
2149
+ * @returns Array of created settlements
2150
+ */
2151
+ async createFromSplits(transactionId, options = {}) {
2152
+ const { scheduledAt = /* @__PURE__ */ new Date(), payoutMethod = "bank_transfer", metadata = {} } = options;
2153
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2154
+ const transaction = await this.models.Transaction.findById(transactionId);
2155
+ if (!transaction) throw new TransactionNotFoundError(transactionId);
2156
+ if (!transaction.splits || transaction.splits.length === 0) throw new ValidationError("Transaction has no splits to settle", { transactionId });
2157
+ const SettlementModel = this.models.Settlement;
2158
+ const settlements = [];
2159
+ for (const split of transaction.splits) {
2160
+ if (split.status === "paid") {
2161
+ this.logger.info("Split already paid, skipping", { splitId: split._id });
2162
+ continue;
2163
+ }
2164
+ const settlement = await SettlementModel.create({
2165
+ organizationId: transaction.organizationId,
2166
+ recipientId: split.recipientId,
2167
+ recipientType: split.recipientType,
2168
+ type: SETTLEMENT_TYPE.SPLIT_PAYOUT,
2169
+ status: SETTLEMENT_STATUS.PENDING,
2170
+ payoutMethod,
2171
+ amount: split.netAmount,
2172
+ currency: transaction.currency,
2173
+ sourceTransactionIds: [transaction._id],
2174
+ sourceSplitIds: [split._id?.toString() || ""],
2175
+ scheduledAt,
2176
+ metadata: {
2177
+ ...metadata,
2178
+ splitType: split.type,
2179
+ transactionCategory: transaction.category
2180
+ }
2181
+ });
2182
+ settlements.push(settlement);
2183
+ }
2184
+ this.events.emit("settlement.created", {
2185
+ settlements,
2186
+ transactionId,
2187
+ count: settlements.length
2188
+ });
2189
+ this.logger.info("Created settlements from splits", {
2190
+ transactionId,
2191
+ count: settlements.length
2192
+ });
2193
+ return settlements;
2194
+ }
2195
+ /**
2196
+ * Schedule a payout
2197
+ *
2198
+ * @param params - Settlement parameters
2199
+ * @returns Created settlement
2200
+ */
2201
+ async schedule(params) {
2202
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2203
+ const { organizationId, recipientId, recipientType, type, amount, currency = "USD", payoutMethod, sourceTransactionIds = [], sourceSplitIds = [], scheduledAt = /* @__PURE__ */ new Date(), bankTransferDetails, mobileWalletDetails, cryptoDetails, notes, metadata = {} } = params;
2204
+ if (amount <= 0) throw new ValidationError("Settlement amount must be positive", { amount });
2205
+ const settlement = await this.models.Settlement.create({
2206
+ organizationId,
2207
+ recipientId,
2208
+ recipientType,
2209
+ type,
2210
+ status: SETTLEMENT_STATUS.PENDING,
2211
+ payoutMethod,
2212
+ amount,
2213
+ currency,
2214
+ sourceTransactionIds,
2215
+ sourceSplitIds,
2216
+ scheduledAt,
2217
+ bankTransferDetails,
2218
+ mobileWalletDetails,
2219
+ cryptoDetails,
2220
+ notes,
2221
+ metadata
2222
+ });
2223
+ this.events.emit("settlement.scheduled", {
2224
+ settlement,
2225
+ scheduledAt
2226
+ });
2227
+ this.logger.info("Settlement scheduled", {
2228
+ settlementId: settlement._id,
2229
+ recipientId,
2230
+ amount
2231
+ });
2232
+ return settlement;
2233
+ }
2234
+ /**
2235
+ * Process pending settlements
2236
+ * Batch process settlements that are due
2237
+ *
2238
+ * @param options - Processing options
2239
+ * @returns Processing result
2240
+ */
2241
+ async processPending(options = {}) {
2242
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2243
+ const { limit = 100, organizationId, payoutMethod, dryRun = false } = options;
2244
+ const SettlementModel = this.models.Settlement;
2245
+ const query = {
2246
+ status: SETTLEMENT_STATUS.PENDING,
2247
+ scheduledAt: { $lte: /* @__PURE__ */ new Date() }
2248
+ };
2249
+ if (organizationId) query.organizationId = organizationId;
2250
+ if (payoutMethod) query.payoutMethod = payoutMethod;
2251
+ const settlements = await SettlementModel.find(query).limit(limit).sort({ scheduledAt: 1 });
2252
+ const result = {
2253
+ processed: 0,
2254
+ succeeded: 0,
2255
+ failed: 0,
2256
+ settlements: [],
2257
+ errors: []
2258
+ };
2259
+ if (dryRun) {
2260
+ this.logger.info("Dry run: would process settlements", { count: settlements.length });
2261
+ result.settlements = settlements;
2262
+ return result;
2263
+ }
2264
+ for (const settlement of settlements) {
2265
+ result.processed++;
2266
+ try {
2267
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.PROCESSING, settlement._id.toString(), {
2268
+ changedBy: "system",
2269
+ reason: "Settlement processing started",
2270
+ metadata: {
2271
+ recipientId: settlement.recipientId,
2272
+ amount: settlement.amount
2273
+ }
2274
+ });
2275
+ settlement.status = SETTLEMENT_STATUS.PROCESSING;
2276
+ settlement.processedAt = /* @__PURE__ */ new Date();
2277
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2278
+ await settlement.save();
2279
+ result.succeeded++;
2280
+ result.settlements.push(settlement);
2281
+ this.events.emit("settlement.processing", {
2282
+ settlement,
2283
+ processedAt: settlement.processedAt
2284
+ });
2285
+ } catch (error) {
2286
+ result.failed++;
2287
+ result.errors.push({
2288
+ settlementId: settlement._id.toString(),
2289
+ error: error.message
2290
+ });
2291
+ this.logger.error("Failed to process settlement", {
2292
+ settlementId: settlement._id,
2293
+ error
2294
+ });
2295
+ }
2296
+ }
2297
+ this.logger.info("Processed settlements", result);
2298
+ return result;
2299
+ }
2300
+ /**
2301
+ * Mark settlement as completed
2302
+ * Call this after bank confirms the transfer
2303
+ *
2304
+ * @param settlementId - Settlement ID
2305
+ * @param details - Completion details
2306
+ * @returns Updated settlement
2307
+ */
2308
+ async complete(settlementId, details = {}) {
2309
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2310
+ const settlement = await this.models.Settlement.findById(settlementId);
2311
+ if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2312
+ 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");
2313
+ const { transferReference, transferredAt = /* @__PURE__ */ new Date(), transactionHash, notes, metadata = {} } = details;
2314
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.COMPLETED, settlement._id.toString(), {
2315
+ changedBy: "system",
2316
+ reason: "Settlement completed successfully",
2317
+ metadata: {
2318
+ transferReference,
2319
+ transferredAt,
2320
+ transactionHash,
2321
+ payoutMethod: settlement.payoutMethod,
2322
+ amount: settlement.amount
2323
+ }
2324
+ });
2325
+ settlement.status = SETTLEMENT_STATUS.COMPLETED;
2326
+ settlement.completedAt = /* @__PURE__ */ new Date();
2327
+ if (settlement.payoutMethod === "bank_transfer" && transferReference) settlement.bankTransferDetails = {
2328
+ ...settlement.bankTransferDetails,
2329
+ transferReference,
2330
+ transferredAt
2331
+ };
2332
+ else if (settlement.payoutMethod === "crypto" && transactionHash) settlement.cryptoDetails = {
2333
+ ...settlement.cryptoDetails,
2334
+ transactionHash,
2335
+ transferredAt
2336
+ };
2337
+ else if (settlement.payoutMethod === "mobile_wallet") settlement.mobileWalletDetails = {
2338
+ ...settlement.mobileWalletDetails,
2339
+ transferredAt
2340
+ };
2341
+ else if (settlement.payoutMethod === "platform_balance") settlement.platformBalanceDetails = {
2342
+ ...settlement.platformBalanceDetails,
2343
+ appliedAt: transferredAt
2344
+ };
2345
+ if (notes) settlement.notes = notes;
2346
+ settlement.metadata = {
2347
+ ...settlement.metadata,
2348
+ ...metadata
2349
+ };
2350
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2351
+ await settlement.save();
2352
+ this.events.emit("settlement.completed", {
2353
+ settlement,
2354
+ completedAt: settlement.completedAt
2355
+ });
2356
+ this.logger.info("Settlement completed", {
2357
+ settlementId: settlement._id,
2358
+ recipientId: settlement.recipientId,
2359
+ amount: settlement.amount
2360
+ });
2361
+ return settlement;
2362
+ }
2363
+ /**
2364
+ * Mark settlement as failed
2365
+ *
2366
+ * @param settlementId - Settlement ID
2367
+ * @param reason - Failure reason
2368
+ * @returns Updated settlement
2369
+ */
2370
+ async fail(settlementId, reason, options = {}) {
2371
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2372
+ const settlement = await this.models.Settlement.findById(settlementId);
2373
+ if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2374
+ const { code, retry = false } = options;
2375
+ if (retry) {
2376
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.PENDING, settlement._id.toString(), {
2377
+ changedBy: "system",
2378
+ reason: `Settlement failed, retrying: ${reason}`,
2379
+ metadata: {
2380
+ failureReason: reason,
2381
+ failureCode: code,
2382
+ retryCount: (settlement.retryCount || 0) + 1,
2383
+ scheduledAt: new Date(Date.now() + 3600 * 1e3)
2384
+ }
2385
+ });
2386
+ settlement.status = SETTLEMENT_STATUS.PENDING;
2387
+ settlement.retryCount = (settlement.retryCount || 0) + 1;
2388
+ settlement.scheduledAt = new Date(Date.now() + 3600 * 1e3);
2389
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2390
+ } else {
2391
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(settlement.status, SETTLEMENT_STATUS.FAILED, settlement._id.toString(), {
2392
+ changedBy: "system",
2393
+ reason: `Settlement failed: ${reason}`,
2394
+ metadata: {
2395
+ failureReason: reason,
2396
+ failureCode: code
2397
+ }
2398
+ });
2399
+ settlement.status = SETTLEMENT_STATUS.FAILED;
2400
+ settlement.failedAt = /* @__PURE__ */ new Date();
2401
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2402
+ }
2403
+ settlement.failureReason = reason;
2404
+ if (code) settlement.failureCode = code;
2405
+ await settlement.save();
2406
+ this.events.emit("settlement.failed", {
2407
+ settlement,
2408
+ reason,
2409
+ code,
2410
+ retry
2411
+ });
2412
+ this.logger.warn("Settlement failed", {
2413
+ settlementId: settlement._id,
2414
+ reason,
2415
+ retry
2416
+ });
2417
+ return settlement;
2418
+ }
2419
+ /**
2420
+ * List settlements with filters
2421
+ *
2422
+ * @param filters - Query filters
2423
+ * @returns Settlements
2424
+ */
2425
+ async list(filters = {}) {
2426
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2427
+ const SettlementModel = this.models.Settlement;
2428
+ const { organizationId, recipientId, status, type, payoutMethod, scheduledAfter, scheduledBefore, limit = 50, skip = 0, sort = { createdAt: -1 } } = filters;
2429
+ const query = {};
2430
+ if (organizationId) query.organizationId = organizationId;
2431
+ if (recipientId) query.recipientId = recipientId;
2432
+ if (status) query.status = Array.isArray(status) ? { $in: status } : status;
2433
+ if (type) query.type = type;
2434
+ if (payoutMethod) query.payoutMethod = payoutMethod;
2435
+ if (scheduledAfter || scheduledBefore) {
2436
+ query.scheduledAt = {};
2437
+ if (scheduledAfter) query.scheduledAt.$gte = scheduledAfter;
2438
+ if (scheduledBefore) query.scheduledAt.$lte = scheduledBefore;
2439
+ }
2440
+ return await SettlementModel.find(query).limit(limit).skip(skip).sort(sort);
2441
+ }
2442
+ /**
2443
+ * Get payout summary for recipient
2444
+ *
2445
+ * @param recipientId - Recipient ID
2446
+ * @param options - Summary options
2447
+ * @returns Settlement summary
2448
+ */
2449
+ async getSummary(recipientId, options = {}) {
2450
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2451
+ const { organizationId, startDate, endDate } = options;
2452
+ const SettlementModel = this.models.Settlement;
2453
+ const query = { recipientId };
2454
+ if (organizationId) query.organizationId = organizationId;
2455
+ if (startDate || endDate) {
2456
+ query.createdAt = {};
2457
+ if (startDate) query.createdAt.$gte = startDate;
2458
+ if (endDate) query.createdAt.$lte = endDate;
2459
+ }
2460
+ const settlements = await SettlementModel.find(query);
2461
+ const summary = {
2462
+ recipientId,
2463
+ totalPending: 0,
2464
+ totalProcessing: 0,
2465
+ totalCompleted: 0,
2466
+ totalFailed: 0,
2467
+ amountPending: 0,
2468
+ amountCompleted: 0,
2469
+ amountFailed: 0,
2470
+ currency: settlements[0]?.currency || "USD",
2471
+ settlements: {
2472
+ pending: 0,
2473
+ processing: 0,
2474
+ completed: 0,
2475
+ failed: 0,
2476
+ cancelled: 0
2477
+ }
2478
+ };
2479
+ for (const settlement of settlements) {
2480
+ summary.settlements[settlement.status]++;
2481
+ if (settlement.status === SETTLEMENT_STATUS.PENDING) {
2482
+ summary.totalPending++;
2483
+ summary.amountPending += settlement.amount;
2484
+ } else if (settlement.status === SETTLEMENT_STATUS.PROCESSING) summary.totalProcessing++;
2485
+ else if (settlement.status === SETTLEMENT_STATUS.COMPLETED) {
2486
+ summary.totalCompleted++;
2487
+ summary.amountCompleted += settlement.amount;
2488
+ if (!summary.lastSettlementDate || settlement.completedAt > summary.lastSettlementDate) summary.lastSettlementDate = settlement.completedAt;
2489
+ } else if (settlement.status === SETTLEMENT_STATUS.FAILED) {
2490
+ summary.totalFailed++;
2491
+ summary.amountFailed += settlement.amount;
2492
+ }
2493
+ }
2494
+ return summary;
2495
+ }
2496
+ /**
2497
+ * Get settlement by ID
2498
+ *
2499
+ * @param settlementId - Settlement ID
2500
+ * @returns Settlement
2501
+ */
2502
+ async get(settlementId) {
2503
+ if (!this.models.Settlement) throw new ModelNotRegisteredError("Settlement");
2504
+ const settlement = await this.models.Settlement.findById(settlementId);
2505
+ if (!settlement) throw new ValidationError("Settlement not found", { settlementId });
2506
+ return settlement;
2507
+ }
2508
+ };
2509
+
2510
+ //#endregion
2511
+ 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 };