@classytic/revenue 1.0.2 → 1.1.2

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 (53) hide show
  1. package/README.md +603 -486
  2. package/dist/application/services/index.d.ts +6 -0
  3. package/dist/application/services/index.js +3288 -0
  4. package/dist/application/services/index.js.map +1 -0
  5. package/dist/core/events.d.ts +455 -0
  6. package/dist/core/events.js +122 -0
  7. package/dist/core/events.js.map +1 -0
  8. package/dist/core/index.d.ts +12 -889
  9. package/dist/core/index.js +2361 -705
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/enums/index.d.ts +54 -25
  12. package/dist/enums/index.js +143 -14
  13. package/dist/enums/index.js.map +1 -1
  14. package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
  15. package/dist/{index-BnJWVXuw.d.ts → index-DxIK0UmZ.d.ts} +281 -26
  16. package/dist/index-EnfKzDbs.d.ts +806 -0
  17. package/dist/{index-ChVD3P9k.d.ts → index-cLJBLUvx.d.ts} +55 -81
  18. package/dist/index.d.ts +16 -15
  19. package/dist/index.js +2583 -2066
  20. package/dist/index.js.map +1 -1
  21. package/dist/infrastructure/plugins/index.d.ts +267 -0
  22. package/dist/infrastructure/plugins/index.js +292 -0
  23. package/dist/infrastructure/plugins/index.js.map +1 -0
  24. package/dist/money-widWVD7r.d.ts +111 -0
  25. package/dist/payment.enums-C1BiGlRa.d.ts +69 -0
  26. package/dist/plugin-Bb9HOE10.d.ts +336 -0
  27. package/dist/providers/index.d.ts +19 -6
  28. package/dist/providers/index.js +22 -3
  29. package/dist/providers/index.js.map +1 -1
  30. package/dist/reconciliation/index.d.ts +215 -0
  31. package/dist/reconciliation/index.js +140 -0
  32. package/dist/reconciliation/index.js.map +1 -0
  33. package/dist/{retry-80lBCmSe.d.ts → retry-D4hFUwVk.d.ts} +1 -41
  34. package/dist/schemas/index.d.ts +1927 -166
  35. package/dist/schemas/index.js +357 -40
  36. package/dist/schemas/index.js.map +1 -1
  37. package/dist/schemas/validation.d.ts +87 -12
  38. package/dist/schemas/validation.js +71 -17
  39. package/dist/schemas/validation.js.map +1 -1
  40. package/dist/settlement.enums-ByC1x0ye.d.ts +130 -0
  41. package/dist/settlement.schema-CpamV7ZY.d.ts +343 -0
  42. package/dist/split.enums-DG3TxQf9.d.ts +42 -0
  43. package/dist/tax-CV8A0sxl.d.ts +60 -0
  44. package/dist/utils/index.d.ts +487 -13
  45. package/dist/utils/index.js +370 -235
  46. package/dist/utils/index.js.map +1 -1
  47. package/package.json +27 -13
  48. package/dist/actions-CwG-b7fR.d.ts +0 -519
  49. package/dist/services/index.d.ts +0 -3
  50. package/dist/services/index.js +0 -1632
  51. package/dist/services/index.js.map +0 -1
  52. package/dist/split.enums-Bh24jw8p.d.ts +0 -255
  53. package/dist/split.schema-DYVP7Wu2.d.ts +0 -958
@@ -0,0 +1,3288 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ // @classytic/revenue - Enterprise Revenue Management System
4
+
5
+
6
+ // src/core/errors.ts
7
+ var RevenueError = class extends Error {
8
+ code;
9
+ retryable;
10
+ metadata;
11
+ constructor(message, code, options = {}) {
12
+ super(message);
13
+ this.name = this.constructor.name;
14
+ this.code = code;
15
+ this.retryable = options.retryable ?? false;
16
+ this.metadata = options.metadata ?? {};
17
+ Error.captureStackTrace(this, this.constructor);
18
+ }
19
+ toJSON() {
20
+ return {
21
+ name: this.name,
22
+ message: this.message,
23
+ code: this.code,
24
+ retryable: this.retryable,
25
+ metadata: this.metadata
26
+ };
27
+ }
28
+ };
29
+ var ConfigurationError = class extends RevenueError {
30
+ constructor(message, metadata = {}) {
31
+ super(message, "CONFIGURATION_ERROR", { retryable: false, metadata });
32
+ }
33
+ };
34
+ var ModelNotRegisteredError = class extends ConfigurationError {
35
+ constructor(modelName) {
36
+ super(
37
+ `Model "${modelName}" is not registered. Register it via createRevenue({ models: { ${modelName}: ... } })`,
38
+ { modelName }
39
+ );
40
+ }
41
+ };
42
+ var ProviderError = class extends RevenueError {
43
+ constructor(message, code, options = {}) {
44
+ super(message, code, options);
45
+ }
46
+ };
47
+ var ProviderNotFoundError = class extends ProviderError {
48
+ constructor(providerName, availableProviders = []) {
49
+ super(
50
+ `Payment provider "${providerName}" not found. Available: ${availableProviders.join(", ")}`,
51
+ "PROVIDER_NOT_FOUND",
52
+ { retryable: false, metadata: { providerName, availableProviders } }
53
+ );
54
+ }
55
+ };
56
+ var ProviderCapabilityError = class extends ProviderError {
57
+ constructor(providerName, capability) {
58
+ super(
59
+ `Provider "${providerName}" does not support ${capability}`,
60
+ "PROVIDER_CAPABILITY_NOT_SUPPORTED",
61
+ { retryable: false, metadata: { providerName, capability } }
62
+ );
63
+ }
64
+ };
65
+ var PaymentIntentCreationError = class extends ProviderError {
66
+ constructor(providerName, originalError) {
67
+ super(
68
+ `Failed to create payment intent with provider "${providerName}": ${originalError.message}`,
69
+ "PAYMENT_INTENT_CREATION_FAILED",
70
+ { retryable: true, metadata: { providerName, originalError: originalError.message } }
71
+ );
72
+ }
73
+ };
74
+ var PaymentVerificationError = class extends ProviderError {
75
+ constructor(paymentIntentId, reason) {
76
+ super(
77
+ `Payment verification failed for intent "${paymentIntentId}": ${reason}`,
78
+ "PAYMENT_VERIFICATION_FAILED",
79
+ { retryable: true, metadata: { paymentIntentId, reason } }
80
+ );
81
+ }
82
+ };
83
+ var NotFoundError = class extends RevenueError {
84
+ constructor(message, code, metadata = {}) {
85
+ super(message, code, { retryable: false, metadata });
86
+ }
87
+ };
88
+ var SubscriptionNotFoundError = class extends NotFoundError {
89
+ constructor(subscriptionId) {
90
+ super(
91
+ `Subscription not found: ${subscriptionId}`,
92
+ "SUBSCRIPTION_NOT_FOUND",
93
+ { subscriptionId }
94
+ );
95
+ }
96
+ };
97
+ var TransactionNotFoundError = class extends NotFoundError {
98
+ constructor(transactionId) {
99
+ super(
100
+ `Transaction not found: ${transactionId}`,
101
+ "TRANSACTION_NOT_FOUND",
102
+ { transactionId }
103
+ );
104
+ }
105
+ };
106
+ var ValidationError = class extends RevenueError {
107
+ constructor(message, metadata = {}) {
108
+ super(message, "VALIDATION_ERROR", { retryable: false, metadata });
109
+ }
110
+ };
111
+ var InvalidAmountError = class extends ValidationError {
112
+ constructor(amount, message) {
113
+ super(
114
+ message ?? `Invalid amount: ${amount}. Amount must be non-negative`,
115
+ { amount }
116
+ );
117
+ }
118
+ };
119
+ var MissingRequiredFieldError = class extends ValidationError {
120
+ constructor(fieldName) {
121
+ super(`Missing required field: ${fieldName}`, { fieldName });
122
+ }
123
+ };
124
+ var StateError = class extends RevenueError {
125
+ constructor(message, code, metadata = {}) {
126
+ super(message, code, { retryable: false, metadata });
127
+ }
128
+ };
129
+ var AlreadyVerifiedError = class extends StateError {
130
+ constructor(transactionId) {
131
+ super(
132
+ `Transaction ${transactionId} is already verified`,
133
+ "ALREADY_VERIFIED",
134
+ { transactionId }
135
+ );
136
+ }
137
+ };
138
+ var InvalidStateTransitionError = class extends StateError {
139
+ constructor(resourceType, resourceId, fromState, toState) {
140
+ super(
141
+ `Invalid state transition for ${resourceType} ${resourceId}: ${fromState} \u2192 ${toState}`,
142
+ "INVALID_STATE_TRANSITION",
143
+ { resourceType, resourceId, fromState, toState }
144
+ );
145
+ }
146
+ };
147
+ var SubscriptionNotActiveError = class extends StateError {
148
+ constructor(subscriptionId, message) {
149
+ super(
150
+ message ?? `Subscription ${subscriptionId} is not active`,
151
+ "SUBSCRIPTION_NOT_ACTIVE",
152
+ { subscriptionId }
153
+ );
154
+ }
155
+ };
156
+ var OperationError = class extends RevenueError {
157
+ constructor(message, code, options = {}) {
158
+ super(message, code, options);
159
+ }
160
+ };
161
+ var RefundNotSupportedError = class extends OperationError {
162
+ constructor(providerName) {
163
+ super(
164
+ `Refunds are not supported by provider "${providerName}"`,
165
+ "REFUND_NOT_SUPPORTED",
166
+ { retryable: false, metadata: { providerName } }
167
+ );
168
+ }
169
+ };
170
+ var RefundError = class extends OperationError {
171
+ constructor(transactionId, reason) {
172
+ super(
173
+ `Refund failed for transaction ${transactionId}: ${reason}`,
174
+ "REFUND_FAILED",
175
+ { retryable: true, metadata: { transactionId, reason } }
176
+ );
177
+ }
178
+ };
179
+
180
+ // src/shared/utils/resilience/retry.ts
181
+ var DEFAULT_CONFIG = {
182
+ maxAttempts: 3,
183
+ baseDelay: 1e3,
184
+ maxDelay: 3e4,
185
+ backoffMultiplier: 2,
186
+ jitter: 0.1,
187
+ retryIf: isRetryableError
188
+ };
189
+ function calculateDelay(attempt, config) {
190
+ const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
191
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
192
+ const jitterRange = cappedDelay * config.jitter;
193
+ const jitter = Math.random() * jitterRange * 2 - jitterRange;
194
+ return Math.round(Math.max(0, cappedDelay + jitter));
195
+ }
196
+ function sleep(ms) {
197
+ return new Promise((resolve) => setTimeout(resolve, ms));
198
+ }
199
+ function isRetryableError(error) {
200
+ if (!(error instanceof Error)) return false;
201
+ if (error.message.includes("ECONNREFUSED")) return true;
202
+ if (error.message.includes("ETIMEDOUT")) return true;
203
+ if (error.message.includes("ENOTFOUND")) return true;
204
+ if (error.message.includes("network")) return true;
205
+ if (error.message.includes("timeout")) return true;
206
+ if (error.message.includes("429")) return true;
207
+ if (error.message.includes("rate limit")) return true;
208
+ if (error.message.includes("500")) return true;
209
+ if (error.message.includes("502")) return true;
210
+ if (error.message.includes("503")) return true;
211
+ if (error.message.includes("504")) return true;
212
+ if ("retryable" in error && error.retryable === true) return true;
213
+ return false;
214
+ }
215
+ async function retry(operation, config = {}) {
216
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
217
+ const state = {
218
+ attempt: 0,
219
+ totalDelay: 0,
220
+ errors: []
221
+ };
222
+ while (state.attempt < fullConfig.maxAttempts) {
223
+ try {
224
+ return await operation();
225
+ } catch (error) {
226
+ state.errors.push(error instanceof Error ? error : new Error(String(error)));
227
+ state.attempt++;
228
+ const shouldRetry = fullConfig.retryIf?.(error) ?? isRetryableError(error);
229
+ if (!shouldRetry || state.attempt >= fullConfig.maxAttempts) {
230
+ throw new RetryExhaustedError(
231
+ `Operation failed after ${state.attempt} attempts`,
232
+ state.errors
233
+ );
234
+ }
235
+ const delay = calculateDelay(state.attempt - 1, fullConfig);
236
+ state.totalDelay += delay;
237
+ fullConfig.onRetry?.(error, state.attempt, delay);
238
+ await sleep(delay);
239
+ }
240
+ }
241
+ throw new RetryExhaustedError(
242
+ `Operation failed after ${state.attempt} attempts`,
243
+ state.errors
244
+ );
245
+ }
246
+ var RetryExhaustedError = class extends Error {
247
+ attempts;
248
+ errors;
249
+ constructor(message, errors) {
250
+ super(message);
251
+ this.name = "RetryExhaustedError";
252
+ this.attempts = errors.length;
253
+ this.errors = errors;
254
+ }
255
+ /**
256
+ * Get the last error
257
+ */
258
+ get lastError() {
259
+ return this.errors[this.errors.length - 1];
260
+ }
261
+ /**
262
+ * Get the first error
263
+ */
264
+ get firstError() {
265
+ return this.errors[0];
266
+ }
267
+ };
268
+
269
+ // src/enums/transaction.enums.ts
270
+ var TRANSACTION_FLOW = {
271
+ INFLOW: "inflow",
272
+ OUTFLOW: "outflow"
273
+ };
274
+ var TRANSACTION_FLOW_VALUES = Object.values(
275
+ TRANSACTION_FLOW
276
+ );
277
+ var TRANSACTION_STATUS = {
278
+ PENDING: "pending",
279
+ PAYMENT_INITIATED: "payment_initiated",
280
+ PROCESSING: "processing",
281
+ REQUIRES_ACTION: "requires_action",
282
+ VERIFIED: "verified",
283
+ COMPLETED: "completed",
284
+ FAILED: "failed",
285
+ CANCELLED: "cancelled",
286
+ EXPIRED: "expired",
287
+ REFUNDED: "refunded",
288
+ PARTIALLY_REFUNDED: "partially_refunded"
289
+ };
290
+ var TRANSACTION_STATUS_VALUES = Object.values(
291
+ TRANSACTION_STATUS
292
+ );
293
+ var LIBRARY_CATEGORIES = {
294
+ SUBSCRIPTION: "subscription",
295
+ PURCHASE: "purchase"
296
+ };
297
+ var LIBRARY_CATEGORY_VALUES = Object.values(
298
+ LIBRARY_CATEGORIES
299
+ );
300
+ new Set(TRANSACTION_FLOW_VALUES);
301
+ new Set(
302
+ TRANSACTION_STATUS_VALUES
303
+ );
304
+ new Set(LIBRARY_CATEGORY_VALUES);
305
+
306
+ // src/shared/utils/validators/category-resolver.ts
307
+ function resolveCategory(entity, monetizationType, categoryMappings = {}) {
308
+ if (entity && categoryMappings[entity]) {
309
+ return categoryMappings[entity];
310
+ }
311
+ switch (monetizationType) {
312
+ case "subscription":
313
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
314
+ // 'subscription'
315
+ case "purchase":
316
+ return LIBRARY_CATEGORIES.PURCHASE;
317
+ // 'purchase'
318
+ default:
319
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
320
+ }
321
+ }
322
+
323
+ // src/shared/utils/calculators/commission.ts
324
+ function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
325
+ if (!commissionRate || commissionRate <= 0) {
326
+ return null;
327
+ }
328
+ if (amount < 0) {
329
+ throw new Error("Transaction amount cannot be negative");
330
+ }
331
+ if (commissionRate < 0 || commissionRate > 1) {
332
+ throw new Error("Commission rate must be between 0 and 1");
333
+ }
334
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
335
+ throw new Error("Gateway fee rate must be between 0 and 1");
336
+ }
337
+ const grossAmount = Math.round(amount * commissionRate);
338
+ const gatewayFeeAmount = Math.round(amount * gatewayFeeRate);
339
+ const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
340
+ return {
341
+ rate: commissionRate,
342
+ grossAmount,
343
+ gatewayFeeRate,
344
+ gatewayFeeAmount,
345
+ netAmount,
346
+ status: "pending"
347
+ };
348
+ }
349
+ function reverseCommission(originalCommission, originalAmount, refundAmount) {
350
+ if (!originalCommission?.netAmount) {
351
+ return null;
352
+ }
353
+ if (!originalAmount || originalAmount <= 0) {
354
+ throw new ValidationError("Original amount must be greater than 0", { originalAmount });
355
+ }
356
+ if (refundAmount < 0) {
357
+ throw new ValidationError("Refund amount cannot be negative", { refundAmount });
358
+ }
359
+ if (refundAmount > originalAmount) {
360
+ throw new ValidationError(
361
+ `Refund amount (${refundAmount}) exceeds original amount (${originalAmount})`,
362
+ { refundAmount, originalAmount }
363
+ );
364
+ }
365
+ const refundRatio = refundAmount / originalAmount;
366
+ const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio);
367
+ const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio);
368
+ const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio);
369
+ return {
370
+ rate: originalCommission.rate,
371
+ grossAmount: reversedGrossAmount,
372
+ gatewayFeeRate: originalCommission.gatewayFeeRate,
373
+ gatewayFeeAmount: reversedGatewayFee,
374
+ netAmount: reversedNetAmount,
375
+ status: "waived"
376
+ // Commission waived due to refund
377
+ };
378
+ }
379
+
380
+ // src/infrastructure/config/resolver.ts
381
+ function getCommissionRate(config, category) {
382
+ if (!config?.commissionRates) return 0;
383
+ if (category in config.commissionRates) {
384
+ return config.commissionRates[category];
385
+ }
386
+ return config.commissionRates["*"] ?? 0;
387
+ }
388
+ function getGatewayFeeRate(config, gateway) {
389
+ if (!config?.gatewayFeeRates) return 0;
390
+ if (gateway in config.gatewayFeeRates) {
391
+ return config.gatewayFeeRates[gateway];
392
+ }
393
+ return config.gatewayFeeRates["*"] ?? 0;
394
+ }
395
+
396
+ // src/enums/monetization.enums.ts
397
+ var MONETIZATION_TYPES = {
398
+ FREE: "free",
399
+ PURCHASE: "purchase",
400
+ SUBSCRIPTION: "subscription"
401
+ };
402
+ var MONETIZATION_TYPE_VALUES = Object.values(
403
+ MONETIZATION_TYPES
404
+ );
405
+ new Set(MONETIZATION_TYPE_VALUES);
406
+
407
+ // src/core/state-machine/StateMachine.ts
408
+ var StateMachine = class {
409
+ /**
410
+ * @param transitions - Map of state → allowed next states
411
+ * @param resourceType - Type of resource (for error messages)
412
+ */
413
+ constructor(transitions, resourceType) {
414
+ this.transitions = transitions;
415
+ this.resourceType = resourceType;
416
+ }
417
+ /**
418
+ * Validate state transition is allowed
419
+ *
420
+ * @param from - Current state
421
+ * @param to - Target state
422
+ * @param resourceId - ID of the resource being transitioned
423
+ * @throws InvalidStateTransitionError if transition is invalid
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * try {
428
+ * stateMachine.validate('pending', 'completed', 'tx_123');
429
+ * } catch (error) {
430
+ * if (error instanceof InvalidStateTransitionError) {
431
+ * console.error('Invalid transition:', error.message);
432
+ * }
433
+ * }
434
+ * ```
435
+ */
436
+ validate(from, to, resourceId) {
437
+ const allowedTransitions = this.transitions.get(from);
438
+ if (!allowedTransitions?.has(to)) {
439
+ throw new InvalidStateTransitionError(
440
+ this.resourceType,
441
+ resourceId,
442
+ from,
443
+ to
444
+ );
445
+ }
446
+ }
447
+ /**
448
+ * Check if transition is valid (non-throwing)
449
+ *
450
+ * @param from - Current state
451
+ * @param to - Target state
452
+ * @returns true if transition is allowed
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * if (stateMachine.canTransition('pending', 'processing')) {
457
+ * // Safe to proceed with transition
458
+ * transaction.status = 'processing';
459
+ * }
460
+ * ```
461
+ */
462
+ canTransition(from, to) {
463
+ return this.transitions.get(from)?.has(to) ?? false;
464
+ }
465
+ /**
466
+ * Get all allowed next states from current state
467
+ *
468
+ * @param from - Current state
469
+ * @returns Array of allowed next states
470
+ *
471
+ * @example
472
+ * ```typescript
473
+ * const nextStates = stateMachine.getAllowedTransitions('pending');
474
+ * console.log(nextStates); // ['processing', 'failed']
475
+ * ```
476
+ */
477
+ getAllowedTransitions(from) {
478
+ return Array.from(this.transitions.get(from) ?? []);
479
+ }
480
+ /**
481
+ * Check if state is terminal (no outgoing transitions)
482
+ *
483
+ * @param state - State to check
484
+ * @returns true if state has no outgoing transitions
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * stateMachine.isTerminalState('completed'); // true
489
+ * stateMachine.isTerminalState('pending'); // false
490
+ * ```
491
+ */
492
+ isTerminalState(state) {
493
+ const transitions = this.transitions.get(state);
494
+ return !transitions || transitions.size === 0;
495
+ }
496
+ /**
497
+ * Get the resource type this state machine manages
498
+ *
499
+ * @returns Resource type string
500
+ */
501
+ getResourceType() {
502
+ return this.resourceType;
503
+ }
504
+ /**
505
+ * Validate state transition and create audit event
506
+ *
507
+ * This is a convenience method that combines validation with audit event creation.
508
+ * Use this when you want to both validate a transition and record it in the audit trail.
509
+ *
510
+ * @param from - Current state
511
+ * @param to - Target state
512
+ * @param resourceId - ID of the resource being transitioned
513
+ * @param context - Optional audit context (who, why, metadata)
514
+ * @returns StateChangeEvent ready to be appended to document metadata
515
+ * @throws InvalidStateTransitionError if transition is invalid
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * import { appendAuditEvent } from '@classytic/revenue';
520
+ *
521
+ * // Validate and create audit event
522
+ * const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
523
+ * transaction.status,
524
+ * 'verified',
525
+ * transaction._id.toString(),
526
+ * {
527
+ * changedBy: 'admin_123',
528
+ * reason: 'Payment verified by payment gateway',
529
+ * metadata: { verificationId: 'ver_abc' }
530
+ * }
531
+ * );
532
+ *
533
+ * // Apply state change
534
+ * transaction.status = 'verified';
535
+ *
536
+ * // Append audit event to metadata
537
+ * Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
538
+ *
539
+ * // Save
540
+ * await transaction.save();
541
+ * ```
542
+ */
543
+ validateAndCreateAuditEvent(from, to, resourceId, context) {
544
+ this.validate(from, to, resourceId);
545
+ return {
546
+ resourceType: this.resourceType,
547
+ resourceId,
548
+ fromState: from,
549
+ toState: to,
550
+ changedAt: /* @__PURE__ */ new Date(),
551
+ changedBy: context?.changedBy,
552
+ reason: context?.reason,
553
+ metadata: context?.metadata
554
+ };
555
+ }
556
+ };
557
+
558
+ // src/enums/subscription.enums.ts
559
+ var SUBSCRIPTION_STATUS = {
560
+ ACTIVE: "active",
561
+ PAUSED: "paused",
562
+ CANCELLED: "cancelled",
563
+ EXPIRED: "expired",
564
+ PENDING: "pending",
565
+ PENDING_RENEWAL: "pending_renewal",
566
+ INACTIVE: "inactive"
567
+ };
568
+ var SUBSCRIPTION_STATUS_VALUES = Object.values(
569
+ SUBSCRIPTION_STATUS
570
+ );
571
+ var PLAN_KEYS = {
572
+ MONTHLY: "monthly",
573
+ QUARTERLY: "quarterly",
574
+ YEARLY: "yearly"
575
+ };
576
+ var PLAN_KEY_VALUES = Object.values(PLAN_KEYS);
577
+ new Set(
578
+ SUBSCRIPTION_STATUS_VALUES
579
+ );
580
+ new Set(PLAN_KEY_VALUES);
581
+
582
+ // src/enums/settlement.enums.ts
583
+ var SETTLEMENT_STATUS = {
584
+ PENDING: "pending",
585
+ PROCESSING: "processing",
586
+ COMPLETED: "completed",
587
+ FAILED: "failed",
588
+ CANCELLED: "cancelled"
589
+ };
590
+ var SETTLEMENT_TYPE = {
591
+ SPLIT_PAYOUT: "split_payout"};
592
+
593
+ // src/enums/escrow.enums.ts
594
+ var HOLD_STATUS = {
595
+ PENDING: "pending",
596
+ HELD: "held",
597
+ RELEASED: "released",
598
+ CANCELLED: "cancelled",
599
+ EXPIRED: "expired",
600
+ PARTIALLY_RELEASED: "partially_released"
601
+ };
602
+ var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
603
+ var RELEASE_REASON = {
604
+ PAYMENT_VERIFIED: "payment_verified",
605
+ MANUAL_RELEASE: "manual_release",
606
+ AUTO_RELEASE: "auto_release",
607
+ DISPUTE_RESOLVED: "dispute_resolved"
608
+ };
609
+ var RELEASE_REASON_VALUES = Object.values(
610
+ RELEASE_REASON
611
+ );
612
+ var HOLD_REASON = {
613
+ PAYMENT_VERIFICATION: "payment_verification",
614
+ FRAUD_CHECK: "fraud_check",
615
+ MANUAL_REVIEW: "manual_review",
616
+ DISPUTE: "dispute",
617
+ COMPLIANCE: "compliance"
618
+ };
619
+ var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
620
+ new Set(HOLD_STATUS_VALUES);
621
+ new Set(RELEASE_REASON_VALUES);
622
+ new Set(HOLD_REASON_VALUES);
623
+
624
+ // src/enums/split.enums.ts
625
+ var SPLIT_TYPE = {
626
+ PLATFORM_COMMISSION: "platform_commission",
627
+ AFFILIATE_COMMISSION: "affiliate_commission",
628
+ REFERRAL_COMMISSION: "referral_commission",
629
+ PARTNER_COMMISSION: "partner_commission",
630
+ CUSTOM: "custom"
631
+ };
632
+ var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
633
+ var SPLIT_STATUS = {
634
+ PENDING: "pending",
635
+ DUE: "due",
636
+ PAID: "paid",
637
+ WAIVED: "waived",
638
+ CANCELLED: "cancelled"
639
+ };
640
+ var SPLIT_STATUS_VALUES = Object.values(
641
+ SPLIT_STATUS
642
+ );
643
+ var PAYOUT_METHOD = {
644
+ BANK_TRANSFER: "bank_transfer",
645
+ MOBILE_WALLET: "mobile_wallet",
646
+ PLATFORM_BALANCE: "platform_balance",
647
+ CRYPTO: "crypto",
648
+ CHECK: "check",
649
+ MANUAL: "manual"
650
+ };
651
+ var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
652
+ new Set(SPLIT_TYPE_VALUES);
653
+ new Set(SPLIT_STATUS_VALUES);
654
+ new Set(PAYOUT_METHOD_VALUES);
655
+
656
+ // src/core/state-machine/definitions.ts
657
+ var TRANSACTION_STATE_MACHINE = new StateMachine(
658
+ /* @__PURE__ */ new Map([
659
+ [
660
+ TRANSACTION_STATUS.PENDING,
661
+ /* @__PURE__ */ new Set([
662
+ TRANSACTION_STATUS.PAYMENT_INITIATED,
663
+ TRANSACTION_STATUS.PROCESSING,
664
+ TRANSACTION_STATUS.VERIFIED,
665
+ // Allow direct verification (manual payments)
666
+ TRANSACTION_STATUS.FAILED,
667
+ TRANSACTION_STATUS.CANCELLED
668
+ ])
669
+ ],
670
+ [
671
+ TRANSACTION_STATUS.PAYMENT_INITIATED,
672
+ /* @__PURE__ */ new Set([
673
+ TRANSACTION_STATUS.PROCESSING,
674
+ TRANSACTION_STATUS.VERIFIED,
675
+ // Allow direct verification (webhook/instant payments)
676
+ TRANSACTION_STATUS.REQUIRES_ACTION,
677
+ TRANSACTION_STATUS.FAILED,
678
+ TRANSACTION_STATUS.CANCELLED
679
+ ])
680
+ ],
681
+ [
682
+ TRANSACTION_STATUS.PROCESSING,
683
+ /* @__PURE__ */ new Set([
684
+ TRANSACTION_STATUS.VERIFIED,
685
+ TRANSACTION_STATUS.REQUIRES_ACTION,
686
+ TRANSACTION_STATUS.FAILED
687
+ ])
688
+ ],
689
+ [
690
+ TRANSACTION_STATUS.REQUIRES_ACTION,
691
+ /* @__PURE__ */ new Set([
692
+ TRANSACTION_STATUS.VERIFIED,
693
+ TRANSACTION_STATUS.FAILED,
694
+ TRANSACTION_STATUS.CANCELLED,
695
+ TRANSACTION_STATUS.EXPIRED
696
+ ])
697
+ ],
698
+ [
699
+ TRANSACTION_STATUS.VERIFIED,
700
+ /* @__PURE__ */ new Set([
701
+ TRANSACTION_STATUS.COMPLETED,
702
+ TRANSACTION_STATUS.REFUNDED,
703
+ TRANSACTION_STATUS.PARTIALLY_REFUNDED,
704
+ TRANSACTION_STATUS.CANCELLED
705
+ ])
706
+ ],
707
+ [
708
+ TRANSACTION_STATUS.COMPLETED,
709
+ /* @__PURE__ */ new Set([
710
+ TRANSACTION_STATUS.REFUNDED,
711
+ TRANSACTION_STATUS.PARTIALLY_REFUNDED
712
+ ])
713
+ ],
714
+ [
715
+ TRANSACTION_STATUS.PARTIALLY_REFUNDED,
716
+ /* @__PURE__ */ new Set([TRANSACTION_STATUS.REFUNDED])
717
+ ],
718
+ // Terminal states
719
+ [TRANSACTION_STATUS.FAILED, /* @__PURE__ */ new Set([])],
720
+ [TRANSACTION_STATUS.REFUNDED, /* @__PURE__ */ new Set([])],
721
+ [TRANSACTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
722
+ [TRANSACTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
723
+ ]),
724
+ "transaction"
725
+ );
726
+ var SUBSCRIPTION_STATE_MACHINE = new StateMachine(
727
+ /* @__PURE__ */ new Map([
728
+ [
729
+ SUBSCRIPTION_STATUS.PENDING,
730
+ /* @__PURE__ */ new Set([
731
+ SUBSCRIPTION_STATUS.ACTIVE,
732
+ SUBSCRIPTION_STATUS.CANCELLED
733
+ ])
734
+ ],
735
+ [
736
+ SUBSCRIPTION_STATUS.ACTIVE,
737
+ /* @__PURE__ */ new Set([
738
+ SUBSCRIPTION_STATUS.PAUSED,
739
+ SUBSCRIPTION_STATUS.PENDING_RENEWAL,
740
+ SUBSCRIPTION_STATUS.CANCELLED,
741
+ SUBSCRIPTION_STATUS.EXPIRED
742
+ ])
743
+ ],
744
+ [
745
+ SUBSCRIPTION_STATUS.PENDING_RENEWAL,
746
+ /* @__PURE__ */ new Set([
747
+ SUBSCRIPTION_STATUS.ACTIVE,
748
+ SUBSCRIPTION_STATUS.CANCELLED,
749
+ SUBSCRIPTION_STATUS.EXPIRED
750
+ ])
751
+ ],
752
+ [
753
+ SUBSCRIPTION_STATUS.PAUSED,
754
+ /* @__PURE__ */ new Set([
755
+ SUBSCRIPTION_STATUS.ACTIVE,
756
+ SUBSCRIPTION_STATUS.CANCELLED
757
+ ])
758
+ ],
759
+ [
760
+ SUBSCRIPTION_STATUS.INACTIVE,
761
+ /* @__PURE__ */ new Set([
762
+ SUBSCRIPTION_STATUS.ACTIVE,
763
+ SUBSCRIPTION_STATUS.CANCELLED
764
+ ])
765
+ ],
766
+ // Terminal states
767
+ [SUBSCRIPTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
768
+ [SUBSCRIPTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
769
+ ]),
770
+ "subscription"
771
+ );
772
+ var SETTLEMENT_STATE_MACHINE = new StateMachine(
773
+ /* @__PURE__ */ new Map([
774
+ [
775
+ SETTLEMENT_STATUS.PENDING,
776
+ /* @__PURE__ */ new Set([
777
+ SETTLEMENT_STATUS.PROCESSING,
778
+ SETTLEMENT_STATUS.CANCELLED
779
+ ])
780
+ ],
781
+ [
782
+ SETTLEMENT_STATUS.PROCESSING,
783
+ /* @__PURE__ */ new Set([
784
+ SETTLEMENT_STATUS.COMPLETED,
785
+ SETTLEMENT_STATUS.FAILED
786
+ ])
787
+ ],
788
+ [
789
+ SETTLEMENT_STATUS.FAILED,
790
+ /* @__PURE__ */ new Set([
791
+ SETTLEMENT_STATUS.PENDING,
792
+ // Allow retry
793
+ SETTLEMENT_STATUS.CANCELLED
794
+ ])
795
+ ],
796
+ // Terminal states
797
+ [SETTLEMENT_STATUS.COMPLETED, /* @__PURE__ */ new Set([])],
798
+ [SETTLEMENT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
799
+ ]),
800
+ "settlement"
801
+ );
802
+ var HOLD_STATE_MACHINE = new StateMachine(
803
+ /* @__PURE__ */ new Map([
804
+ [
805
+ HOLD_STATUS.HELD,
806
+ /* @__PURE__ */ new Set([
807
+ HOLD_STATUS.RELEASED,
808
+ HOLD_STATUS.PARTIALLY_RELEASED,
809
+ HOLD_STATUS.CANCELLED,
810
+ HOLD_STATUS.EXPIRED
811
+ ])
812
+ ],
813
+ [
814
+ HOLD_STATUS.PARTIALLY_RELEASED,
815
+ /* @__PURE__ */ new Set([
816
+ HOLD_STATUS.RELEASED,
817
+ HOLD_STATUS.CANCELLED
818
+ ])
819
+ ],
820
+ // Terminal states
821
+ [HOLD_STATUS.RELEASED, /* @__PURE__ */ new Set([])],
822
+ [HOLD_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
823
+ [HOLD_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
824
+ ]),
825
+ "escrow_hold"
826
+ );
827
+
828
+ // src/infrastructure/audit/types.ts
829
+ function appendAuditEvent(document, event) {
830
+ const stateHistory = document.metadata?.stateHistory ?? [];
831
+ return {
832
+ ...document,
833
+ metadata: {
834
+ ...document.metadata,
835
+ stateHistory: [...stateHistory, event]
836
+ }
837
+ };
838
+ }
839
+
840
+ // src/application/services/monetization.service.ts
841
+ var MonetizationService = class {
842
+ models;
843
+ providers;
844
+ config;
845
+ plugins;
846
+ logger;
847
+ events;
848
+ retryConfig;
849
+ circuitBreaker;
850
+ constructor(container) {
851
+ this.models = container.get("models");
852
+ this.providers = container.get("providers");
853
+ this.config = container.get("config");
854
+ this.plugins = container.get("plugins");
855
+ this.logger = container.get("logger");
856
+ this.events = container.get("events");
857
+ this.retryConfig = container.get("retryConfig");
858
+ this.circuitBreaker = container.get("circuitBreaker");
859
+ }
860
+ /**
861
+ * Create plugin context for hook execution
862
+ * @private
863
+ */
864
+ getPluginContext(idempotencyKey) {
865
+ return {
866
+ events: this.events,
867
+ logger: this.logger,
868
+ storage: /* @__PURE__ */ new Map(),
869
+ meta: {
870
+ idempotencyKey: idempotencyKey ?? void 0,
871
+ requestId: nanoid(),
872
+ timestamp: /* @__PURE__ */ new Date()
873
+ }
874
+ };
875
+ }
876
+ /**
877
+ * Execute provider call with retry and circuit breaker protection
878
+ * @private
879
+ */
880
+ async executeProviderCall(operation, operationName) {
881
+ const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
882
+ if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
883
+ return retry(withCircuitBreaker, {
884
+ ...this.retryConfig,
885
+ onRetry: (error, attempt, delay) => {
886
+ this.logger.warn(
887
+ `[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
888
+ error
889
+ );
890
+ this.retryConfig.onRetry?.(error, attempt, delay);
891
+ }
892
+ });
893
+ }
894
+ return withCircuitBreaker();
895
+ }
896
+ /**
897
+ * Create a new monetization (purchase, subscription, or free item)
898
+ *
899
+ * @param params - Monetization parameters
900
+ *
901
+ * @example
902
+ * // One-time purchase
903
+ * await revenue.monetization.create({
904
+ * data: {
905
+ * organizationId: '...',
906
+ * customerId: '...',
907
+ * sourceId: order._id,
908
+ * sourceModel: 'Order',
909
+ * },
910
+ * planKey: 'one_time',
911
+ * monetizationType: 'purchase',
912
+ * gateway: 'bkash',
913
+ * amount: 1500,
914
+ * });
915
+ *
916
+ * // Recurring subscription
917
+ * await revenue.monetization.create({
918
+ * data: {
919
+ * organizationId: '...',
920
+ * customerId: '...',
921
+ * sourceId: subscription._id,
922
+ * sourceModel: 'Subscription',
923
+ * },
924
+ * planKey: 'monthly',
925
+ * monetizationType: 'subscription',
926
+ * gateway: 'stripe',
927
+ * amount: 2000,
928
+ * });
929
+ *
930
+ * @returns Result with subscription, transaction, and paymentIntent
931
+ */
932
+ async create(params) {
933
+ return this.plugins.executeHook(
934
+ "monetization.create.before",
935
+ this.getPluginContext(params.idempotencyKey),
936
+ params,
937
+ async () => {
938
+ const {
939
+ data,
940
+ planKey,
941
+ amount,
942
+ currency = "BDT",
943
+ gateway = "manual",
944
+ entity = null,
945
+ monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
946
+ paymentData,
947
+ metadata = {},
948
+ idempotencyKey = null
949
+ } = params;
950
+ if (!planKey) {
951
+ throw new MissingRequiredFieldError("planKey");
952
+ }
953
+ if (amount < 0) {
954
+ throw new InvalidAmountError(amount);
955
+ }
956
+ const isFree = amount === 0;
957
+ const provider = this.providers[gateway];
958
+ if (!provider) {
959
+ throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
960
+ }
961
+ let paymentIntent = null;
962
+ let transaction = null;
963
+ if (!isFree) {
964
+ try {
965
+ paymentIntent = await this.executeProviderCall(
966
+ () => provider.createIntent({
967
+ amount,
968
+ currency,
969
+ metadata: {
970
+ ...metadata,
971
+ type: "subscription",
972
+ planKey
973
+ }
974
+ }),
975
+ `${gateway}.createIntent`
976
+ );
977
+ } catch (error) {
978
+ throw new PaymentIntentCreationError(gateway, error);
979
+ }
980
+ const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
981
+ const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_FLOW.INFLOW;
982
+ const commissionRate = getCommissionRate(this.config, category);
983
+ const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
984
+ const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
985
+ const tax = params.tax;
986
+ const TransactionModel = this.models.Transaction;
987
+ const baseAmount = tax?.pricesIncludeTax ? tax.baseAmount : amount;
988
+ const feeAmount = commission?.gatewayFeeAmount || 0;
989
+ const taxAmount = tax?.taxAmount || 0;
990
+ const netAmount = baseAmount - feeAmount - taxAmount;
991
+ transaction = await TransactionModel.create({
992
+ organizationId: data.organizationId,
993
+ customerId: data.customerId ?? null,
994
+ // ✅ UNIFIED: Use category as type directly
995
+ type: category,
996
+ // 'subscription', 'purchase', etc.
997
+ flow: transactionFlow,
998
+ // ✅ Use config-driven transaction type
999
+ // Auto-tagging (middleware will handle, but we can set explicitly)
1000
+ tags: category === "subscription" ? ["recurring", "subscription"] : [],
1001
+ // ✅ UNIFIED: Amount structure
1002
+ // When prices include tax, use baseAmount (tax already extracted)
1003
+ amount: baseAmount,
1004
+ currency,
1005
+ fee: feeAmount,
1006
+ tax: taxAmount,
1007
+ net: netAmount,
1008
+ // ✅ UNIFIED: Tax details (if tax plugin used)
1009
+ ...tax && {
1010
+ taxDetails: {
1011
+ type: tax.type === "collected" ? "sales_tax" : tax.type === "paid" ? "vat" : "none",
1012
+ rate: tax.rate || 0,
1013
+ isInclusive: tax.pricesIncludeTax || false
1014
+ }
1015
+ },
1016
+ method: paymentData?.method ?? "manual",
1017
+ status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1018
+ // ✅ Map 'succeeded' to valid TransactionStatusValue
1019
+ gateway: {
1020
+ type: gateway,
1021
+ // Gateway/provider type (e.g., 'stripe', 'manual')
1022
+ provider: gateway,
1023
+ sessionId: paymentIntent.sessionId,
1024
+ paymentIntentId: paymentIntent.paymentIntentId,
1025
+ chargeId: paymentIntent.id,
1026
+ metadata: paymentIntent.metadata
1027
+ },
1028
+ paymentDetails: {
1029
+ ...paymentData
1030
+ },
1031
+ // Commission (for marketplace/splits)
1032
+ ...commission && { commission },
1033
+ // ✅ UNIFIED: Source reference (renamed from reference)
1034
+ ...data.sourceId && { sourceId: data.sourceId },
1035
+ ...data.sourceModel && { sourceModel: data.sourceModel },
1036
+ metadata: {
1037
+ ...metadata,
1038
+ planKey,
1039
+ entity,
1040
+ monetizationType,
1041
+ paymentIntentId: paymentIntent.id
1042
+ },
1043
+ idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
1044
+ });
1045
+ }
1046
+ let subscription = null;
1047
+ if (this.models.Subscription) {
1048
+ const SubscriptionModel = this.models.Subscription;
1049
+ const result2 = await this.plugins.executeHook(
1050
+ "subscription.create.before",
1051
+ this.getPluginContext(idempotencyKey),
1052
+ {
1053
+ subscriptionId: void 0,
1054
+ // Not yet created - populated in after hook
1055
+ planKey,
1056
+ customerId: data.customerId,
1057
+ organizationId: data.organizationId,
1058
+ entity
1059
+ },
1060
+ async () => {
1061
+ const subscriptionData = {
1062
+ organizationId: data.organizationId,
1063
+ customerId: data.customerId ?? null,
1064
+ planKey,
1065
+ amount,
1066
+ currency,
1067
+ status: isFree ? "active" : "pending",
1068
+ isActive: isFree,
1069
+ gateway,
1070
+ transactionId: transaction?._id ?? null,
1071
+ paymentIntentId: paymentIntent?.id ?? null,
1072
+ metadata: {
1073
+ ...metadata,
1074
+ isFree,
1075
+ entity,
1076
+ monetizationType
1077
+ },
1078
+ ...data
1079
+ };
1080
+ delete subscriptionData.sourceId;
1081
+ delete subscriptionData.sourceModel;
1082
+ const sub = await SubscriptionModel.create(subscriptionData);
1083
+ await this.plugins.executeHook(
1084
+ "subscription.create.after",
1085
+ this.getPluginContext(idempotencyKey),
1086
+ {
1087
+ subscriptionId: sub._id.toString(),
1088
+ planKey,
1089
+ customerId: data.customerId,
1090
+ organizationId: data.organizationId,
1091
+ entity
1092
+ },
1093
+ async () => ({ subscription: sub, transaction })
1094
+ );
1095
+ return { subscription: sub, transaction };
1096
+ }
1097
+ );
1098
+ subscription = result2.subscription;
1099
+ }
1100
+ this.events.emit("monetization.created", {
1101
+ monetizationType,
1102
+ subscription: subscription ?? void 0,
1103
+ transaction: transaction ?? void 0,
1104
+ paymentIntent: paymentIntent ?? void 0
1105
+ });
1106
+ if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
1107
+ if (transaction) {
1108
+ this.events.emit("purchase.created", {
1109
+ monetizationType,
1110
+ subscription: subscription ?? void 0,
1111
+ transaction,
1112
+ paymentIntent: paymentIntent ?? void 0
1113
+ });
1114
+ }
1115
+ } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
1116
+ if (subscription) {
1117
+ this.events.emit("subscription.created", {
1118
+ subscriptionId: subscription._id.toString(),
1119
+ subscription,
1120
+ transactionId: transaction?._id?.toString()
1121
+ });
1122
+ }
1123
+ } else if (monetizationType === MONETIZATION_TYPES.FREE) {
1124
+ this.events.emit("free.created", {
1125
+ monetizationType,
1126
+ subscription: subscription ?? void 0,
1127
+ transaction: transaction ?? void 0,
1128
+ paymentIntent: paymentIntent ?? void 0
1129
+ });
1130
+ }
1131
+ const result = {
1132
+ subscription,
1133
+ transaction,
1134
+ paymentIntent
1135
+ };
1136
+ return this.plugins.executeHook(
1137
+ "monetization.create.after",
1138
+ this.getPluginContext(params.idempotencyKey),
1139
+ params,
1140
+ async () => result
1141
+ );
1142
+ }
1143
+ );
1144
+ }
1145
+ /**
1146
+ * Activate subscription after payment verification
1147
+ *
1148
+ * @param subscriptionId - Subscription ID or transaction ID
1149
+ * @param options - Activation options
1150
+ * @returns Updated subscription
1151
+ */
1152
+ async activate(subscriptionId, options = {}) {
1153
+ return this.plugins.executeHook(
1154
+ "subscription.activate.before",
1155
+ this.getPluginContext(),
1156
+ { subscriptionId, ...options },
1157
+ async () => {
1158
+ const { timestamp = /* @__PURE__ */ new Date() } = options;
1159
+ if (!this.models.Subscription) {
1160
+ throw new ModelNotRegisteredError("Subscription");
1161
+ }
1162
+ const SubscriptionModel = this.models.Subscription;
1163
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1164
+ if (!subscription) {
1165
+ throw new SubscriptionNotFoundError(subscriptionId);
1166
+ }
1167
+ if (subscription.isActive) {
1168
+ this.logger.warn("Subscription already active", { subscriptionId });
1169
+ return subscription;
1170
+ }
1171
+ const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
1172
+ const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
1173
+ subscription.status,
1174
+ "active",
1175
+ subscription._id.toString(),
1176
+ {
1177
+ changedBy: "system",
1178
+ reason: `Subscription activated for plan: ${subscription.planKey}`,
1179
+ metadata: { planKey: subscription.planKey, startDate: timestamp, endDate: periodEnd }
1180
+ }
1181
+ );
1182
+ subscription.isActive = true;
1183
+ subscription.status = "active";
1184
+ subscription.startDate = timestamp;
1185
+ subscription.endDate = periodEnd;
1186
+ subscription.activatedAt = timestamp;
1187
+ Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1188
+ await subscription.save();
1189
+ this.events.emit("subscription.activated", {
1190
+ subscription,
1191
+ activatedAt: timestamp
1192
+ });
1193
+ return this.plugins.executeHook(
1194
+ "subscription.activate.after",
1195
+ this.getPluginContext(),
1196
+ { subscriptionId, ...options },
1197
+ async () => subscription
1198
+ );
1199
+ }
1200
+ );
1201
+ }
1202
+ /**
1203
+ * Renew subscription
1204
+ *
1205
+ * @param subscriptionId - Subscription ID
1206
+ * @param params - Renewal parameters
1207
+ * @returns { subscription, transaction, paymentIntent }
1208
+ */
1209
+ async renew(subscriptionId, params = {}) {
1210
+ const {
1211
+ gateway = "manual",
1212
+ entity = null,
1213
+ paymentData,
1214
+ metadata = {},
1215
+ idempotencyKey = null
1216
+ } = params;
1217
+ if (!this.models.Subscription) {
1218
+ throw new ModelNotRegisteredError("Subscription");
1219
+ }
1220
+ const SubscriptionModel = this.models.Subscription;
1221
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1222
+ if (!subscription) {
1223
+ throw new SubscriptionNotFoundError(subscriptionId);
1224
+ }
1225
+ if (subscription.amount === 0) {
1226
+ throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
1227
+ }
1228
+ const provider = this.providers[gateway];
1229
+ if (!provider) {
1230
+ throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1231
+ }
1232
+ let paymentIntent = null;
1233
+ try {
1234
+ paymentIntent = await provider.createIntent({
1235
+ amount: subscription.amount,
1236
+ currency: subscription.currency ?? "BDT",
1237
+ metadata: {
1238
+ ...metadata,
1239
+ type: "subscription_renewal",
1240
+ subscriptionId: subscription._id.toString()
1241
+ }
1242
+ });
1243
+ } catch (error) {
1244
+ this.logger.error("Failed to create payment intent for renewal:", error);
1245
+ throw new PaymentIntentCreationError(gateway, error);
1246
+ }
1247
+ const effectiveEntity = entity ?? subscription.metadata?.entity;
1248
+ const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
1249
+ const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
1250
+ const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_FLOW.INFLOW;
1251
+ const commissionRate = getCommissionRate(this.config, category);
1252
+ const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
1253
+ const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
1254
+ const feeAmount = commission?.gatewayFeeAmount || 0;
1255
+ const netAmount = subscription.amount - feeAmount;
1256
+ const TransactionModel = this.models.Transaction;
1257
+ const transaction = await TransactionModel.create({
1258
+ organizationId: subscription.organizationId,
1259
+ customerId: subscription.customerId,
1260
+ // ✅ UNIFIED: Use category as type directly
1261
+ type: category,
1262
+ // 'subscription', etc.
1263
+ flow: transactionFlow,
1264
+ // ✅ Use config-driven transaction type
1265
+ tags: ["recurring", "subscription", "renewal"],
1266
+ // ✅ UNIFIED: Amount structure
1267
+ amount: subscription.amount,
1268
+ currency: subscription.currency ?? "BDT",
1269
+ fee: feeAmount,
1270
+ tax: 0,
1271
+ // Tax plugin would add this
1272
+ net: netAmount,
1273
+ method: paymentData?.method ?? "manual",
1274
+ status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1275
+ // ✅ Map 'succeeded' to valid TransactionStatusValue
1276
+ gateway: {
1277
+ provider: gateway,
1278
+ sessionId: paymentIntent.sessionId,
1279
+ paymentIntentId: paymentIntent.paymentIntentId,
1280
+ chargeId: paymentIntent.id,
1281
+ metadata: paymentIntent.metadata
1282
+ },
1283
+ paymentDetails: {
1284
+ provider: gateway,
1285
+ ...paymentData
1286
+ },
1287
+ // Commission (for marketplace/splits)
1288
+ ...commission && { commission },
1289
+ // ✅ UNIFIED: Source reference (renamed from reference)
1290
+ sourceId: subscription._id,
1291
+ sourceModel: "Subscription",
1292
+ metadata: {
1293
+ ...metadata,
1294
+ subscriptionId: subscription._id.toString(),
1295
+ // Keep for backward compat
1296
+ entity: effectiveEntity,
1297
+ monetizationType: effectiveMonetizationType,
1298
+ isRenewal: true,
1299
+ paymentIntentId: paymentIntent.id
1300
+ },
1301
+ idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
1302
+ });
1303
+ subscription.status = "pending_renewal";
1304
+ subscription.renewalTransactionId = transaction._id;
1305
+ subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
1306
+ await subscription.save();
1307
+ this.events.emit("subscription.renewed", {
1308
+ subscription,
1309
+ transaction,
1310
+ paymentIntent: paymentIntent ?? void 0,
1311
+ renewalCount: subscription.renewalCount
1312
+ });
1313
+ return {
1314
+ subscription,
1315
+ transaction,
1316
+ paymentIntent
1317
+ };
1318
+ }
1319
+ /**
1320
+ * Cancel subscription
1321
+ *
1322
+ * @param subscriptionId - Subscription ID
1323
+ * @param options - Cancellation options
1324
+ * @returns Updated subscription
1325
+ */
1326
+ async cancel(subscriptionId, options = {}) {
1327
+ return this.plugins.executeHook(
1328
+ "subscription.cancel.before",
1329
+ this.getPluginContext(),
1330
+ { subscriptionId, ...options },
1331
+ async () => {
1332
+ const { immediate = false, reason = null } = options;
1333
+ if (!this.models.Subscription) {
1334
+ throw new ModelNotRegisteredError("Subscription");
1335
+ }
1336
+ const SubscriptionModel = this.models.Subscription;
1337
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1338
+ if (!subscription) {
1339
+ throw new SubscriptionNotFoundError(subscriptionId);
1340
+ }
1341
+ const now = /* @__PURE__ */ new Date();
1342
+ if (immediate) {
1343
+ const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
1344
+ subscription.status,
1345
+ "cancelled",
1346
+ subscription._id.toString(),
1347
+ {
1348
+ changedBy: "system",
1349
+ reason: `Subscription cancelled immediately${reason ? ": " + reason : ""}`,
1350
+ metadata: { cancellationReason: reason, immediate: true }
1351
+ }
1352
+ );
1353
+ subscription.isActive = false;
1354
+ subscription.status = "cancelled";
1355
+ subscription.canceledAt = now;
1356
+ subscription.cancellationReason = reason;
1357
+ Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1358
+ } else {
1359
+ subscription.cancelAt = subscription.endDate ?? now;
1360
+ subscription.cancellationReason = reason;
1361
+ }
1362
+ await subscription.save();
1363
+ this.events.emit("subscription.cancelled", {
1364
+ subscription,
1365
+ immediate,
1366
+ reason: reason ?? void 0,
1367
+ canceledAt: immediate ? now : subscription.cancelAt
1368
+ });
1369
+ return this.plugins.executeHook(
1370
+ "subscription.cancel.after",
1371
+ this.getPluginContext(),
1372
+ { subscriptionId, ...options },
1373
+ async () => subscription
1374
+ );
1375
+ }
1376
+ );
1377
+ }
1378
+ /**
1379
+ * Pause subscription
1380
+ *
1381
+ * @param subscriptionId - Subscription ID
1382
+ * @param options - Pause options
1383
+ * @returns Updated subscription
1384
+ */
1385
+ async pause(subscriptionId, options = {}) {
1386
+ return this.plugins.executeHook(
1387
+ "subscription.pause.before",
1388
+ this.getPluginContext(),
1389
+ { subscriptionId, ...options },
1390
+ async () => {
1391
+ const { reason = null } = options;
1392
+ if (!this.models.Subscription) {
1393
+ throw new ModelNotRegisteredError("Subscription");
1394
+ }
1395
+ const SubscriptionModel = this.models.Subscription;
1396
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1397
+ if (!subscription) {
1398
+ throw new SubscriptionNotFoundError(subscriptionId);
1399
+ }
1400
+ if (!subscription.isActive) {
1401
+ throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
1402
+ }
1403
+ const pausedAt = /* @__PURE__ */ new Date();
1404
+ const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
1405
+ subscription.status,
1406
+ "paused",
1407
+ subscription._id.toString(),
1408
+ {
1409
+ changedBy: "system",
1410
+ reason: `Subscription paused${reason ? ": " + reason : ""}`,
1411
+ metadata: { pauseReason: reason, pausedAt }
1412
+ }
1413
+ );
1414
+ subscription.isActive = false;
1415
+ subscription.status = "paused";
1416
+ subscription.pausedAt = pausedAt;
1417
+ subscription.pauseReason = reason;
1418
+ Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1419
+ await subscription.save();
1420
+ this.events.emit("subscription.paused", {
1421
+ subscription,
1422
+ reason: reason ?? void 0,
1423
+ pausedAt
1424
+ });
1425
+ return this.plugins.executeHook(
1426
+ "subscription.pause.after",
1427
+ this.getPluginContext(),
1428
+ { subscriptionId, ...options },
1429
+ async () => subscription
1430
+ );
1431
+ }
1432
+ );
1433
+ }
1434
+ /**
1435
+ * Resume subscription
1436
+ *
1437
+ * @param subscriptionId - Subscription ID
1438
+ * @param options - Resume options
1439
+ * @returns Updated subscription
1440
+ */
1441
+ async resume(subscriptionId, options = {}) {
1442
+ return this.plugins.executeHook(
1443
+ "subscription.resume.before",
1444
+ this.getPluginContext(),
1445
+ { subscriptionId, ...options },
1446
+ async () => {
1447
+ const { extendPeriod = false } = options;
1448
+ if (!this.models.Subscription) {
1449
+ throw new ModelNotRegisteredError("Subscription");
1450
+ }
1451
+ const SubscriptionModel = this.models.Subscription;
1452
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1453
+ if (!subscription) {
1454
+ throw new SubscriptionNotFoundError(subscriptionId);
1455
+ }
1456
+ if (!subscription.pausedAt) {
1457
+ throw new InvalidStateTransitionError(
1458
+ "resume",
1459
+ "paused",
1460
+ subscription.status,
1461
+ "Only paused subscriptions can be resumed"
1462
+ );
1463
+ }
1464
+ const now = /* @__PURE__ */ new Date();
1465
+ const pausedAt = new Date(subscription.pausedAt);
1466
+ const pauseDuration = now.getTime() - pausedAt.getTime();
1467
+ const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
1468
+ subscription.status,
1469
+ "active",
1470
+ subscription._id.toString(),
1471
+ {
1472
+ changedBy: "system",
1473
+ reason: "Subscription resumed from paused state",
1474
+ metadata: {
1475
+ pausedAt,
1476
+ pauseDuration,
1477
+ extendPeriod,
1478
+ newEndDate: extendPeriod && subscription.endDate ? new Date(new Date(subscription.endDate).getTime() + pauseDuration) : void 0
1479
+ }
1480
+ }
1481
+ );
1482
+ subscription.isActive = true;
1483
+ subscription.status = "active";
1484
+ subscription.pausedAt = null;
1485
+ subscription.pauseReason = null;
1486
+ if (extendPeriod && subscription.endDate) {
1487
+ const currentEnd = new Date(subscription.endDate);
1488
+ subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1489
+ }
1490
+ Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
1491
+ await subscription.save();
1492
+ this.events.emit("subscription.resumed", {
1493
+ subscription,
1494
+ extendPeriod,
1495
+ pauseDuration,
1496
+ resumedAt: now
1497
+ });
1498
+ return this.plugins.executeHook(
1499
+ "subscription.resume.after",
1500
+ this.getPluginContext(),
1501
+ { subscriptionId, ...options },
1502
+ async () => subscription
1503
+ );
1504
+ }
1505
+ );
1506
+ }
1507
+ /**
1508
+ * List subscriptions with filters
1509
+ *
1510
+ * @param filters - Query filters
1511
+ * @param options - Query options (limit, skip, sort)
1512
+ * @returns Subscriptions
1513
+ */
1514
+ async list(filters = {}, options = {}) {
1515
+ if (!this.models.Subscription) {
1516
+ throw new ModelNotRegisteredError("Subscription");
1517
+ }
1518
+ const SubscriptionModel = this.models.Subscription;
1519
+ const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1520
+ const subscriptions = await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
1521
+ return subscriptions;
1522
+ }
1523
+ /**
1524
+ * Get subscription by ID
1525
+ *
1526
+ * @param subscriptionId - Subscription ID
1527
+ * @returns Subscription
1528
+ */
1529
+ async get(subscriptionId) {
1530
+ if (!this.models.Subscription) {
1531
+ throw new ModelNotRegisteredError("Subscription");
1532
+ }
1533
+ const SubscriptionModel = this.models.Subscription;
1534
+ const subscription = await SubscriptionModel.findById(subscriptionId);
1535
+ if (!subscription) {
1536
+ throw new SubscriptionNotFoundError(subscriptionId);
1537
+ }
1538
+ return subscription;
1539
+ }
1540
+ /**
1541
+ * Calculate period end date based on plan key
1542
+ * @private
1543
+ */
1544
+ _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
1545
+ const start = new Date(startDate);
1546
+ const end = new Date(start);
1547
+ switch (planKey) {
1548
+ case "monthly":
1549
+ end.setMonth(end.getMonth() + 1);
1550
+ break;
1551
+ case "quarterly":
1552
+ end.setMonth(end.getMonth() + 3);
1553
+ break;
1554
+ case "yearly":
1555
+ end.setFullYear(end.getFullYear() + 1);
1556
+ break;
1557
+ default:
1558
+ end.setDate(end.getDate() + 30);
1559
+ }
1560
+ return end;
1561
+ }
1562
+ };
1563
+ var PaymentService = class {
1564
+ models;
1565
+ providers;
1566
+ config;
1567
+ plugins;
1568
+ logger;
1569
+ events;
1570
+ retryConfig;
1571
+ circuitBreaker;
1572
+ constructor(container) {
1573
+ this.models = container.get("models");
1574
+ this.providers = container.get("providers");
1575
+ this.config = container.get("config");
1576
+ this.plugins = container.get("plugins");
1577
+ this.logger = container.get("logger");
1578
+ this.events = container.get("events");
1579
+ this.retryConfig = container.get("retryConfig");
1580
+ this.circuitBreaker = container.get("circuitBreaker");
1581
+ }
1582
+ /**
1583
+ * Create plugin context for hook execution
1584
+ * @private
1585
+ */
1586
+ getPluginContext(idempotencyKey) {
1587
+ return {
1588
+ events: this.events,
1589
+ logger: this.logger,
1590
+ storage: /* @__PURE__ */ new Map(),
1591
+ meta: {
1592
+ idempotencyKey,
1593
+ requestId: nanoid(),
1594
+ timestamp: /* @__PURE__ */ new Date()
1595
+ }
1596
+ };
1597
+ }
1598
+ /**
1599
+ * Execute provider call with retry and circuit breaker protection
1600
+ * @private
1601
+ */
1602
+ async executeProviderCall(operation, operationName) {
1603
+ const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
1604
+ if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
1605
+ return retry(withCircuitBreaker, {
1606
+ ...this.retryConfig,
1607
+ onRetry: (error, attempt, delay) => {
1608
+ this.logger.warn(
1609
+ `[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
1610
+ error
1611
+ );
1612
+ this.retryConfig.onRetry?.(error, attempt, delay);
1613
+ }
1614
+ });
1615
+ }
1616
+ return withCircuitBreaker();
1617
+ }
1618
+ /**
1619
+ * Verify a payment
1620
+ *
1621
+ * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1622
+ * @param options - Verification options
1623
+ * @returns { transaction, status }
1624
+ */
1625
+ async verify(paymentIntentId, options = {}) {
1626
+ return this.plugins.executeHook(
1627
+ "payment.verify.before",
1628
+ this.getPluginContext(),
1629
+ { id: paymentIntentId, ...options },
1630
+ async () => {
1631
+ const { verifiedBy = null } = options;
1632
+ const TransactionModel = this.models.Transaction;
1633
+ const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1634
+ if (!transaction) {
1635
+ throw new TransactionNotFoundError(paymentIntentId);
1636
+ }
1637
+ if (transaction.status === "verified" || transaction.status === "completed") {
1638
+ throw new AlreadyVerifiedError(transaction._id.toString());
1639
+ }
1640
+ const gatewayType = transaction.gateway?.type ?? "manual";
1641
+ const provider = this.providers[gatewayType];
1642
+ if (!provider) {
1643
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1644
+ }
1645
+ let paymentResult = null;
1646
+ try {
1647
+ const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
1648
+ paymentResult = await this.executeProviderCall(
1649
+ () => provider.verifyPayment(actualIntentId),
1650
+ `${gatewayType}.verifyPayment`
1651
+ );
1652
+ } catch (error) {
1653
+ this.logger.error("Payment verification failed:", error);
1654
+ const auditEvent2 = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
1655
+ transaction.status,
1656
+ "failed",
1657
+ transaction._id.toString(),
1658
+ {
1659
+ changedBy: "system",
1660
+ reason: `Payment verification failed: ${error.message}`,
1661
+ metadata: { error: error.message }
1662
+ }
1663
+ );
1664
+ transaction.status = "failed";
1665
+ transaction.failureReason = error.message;
1666
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent2));
1667
+ transaction.metadata = {
1668
+ ...transaction.metadata,
1669
+ verificationError: error.message,
1670
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
1671
+ };
1672
+ await transaction.save();
1673
+ this.events.emit("payment.failed", {
1674
+ transaction,
1675
+ error: error.message,
1676
+ provider: gatewayType,
1677
+ paymentIntentId
1678
+ });
1679
+ throw new PaymentVerificationError(paymentIntentId, error.message);
1680
+ }
1681
+ if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
1682
+ throw new ValidationError(
1683
+ `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
1684
+ { expected: transaction.amount, actual: paymentResult.amount }
1685
+ );
1686
+ }
1687
+ if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
1688
+ throw new ValidationError(
1689
+ `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
1690
+ { expected: transaction.currency, actual: paymentResult.currency }
1691
+ );
1692
+ }
1693
+ const newStatus = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1694
+ const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
1695
+ transaction.status,
1696
+ newStatus,
1697
+ transaction._id.toString(),
1698
+ {
1699
+ changedBy: verifiedBy ?? "system",
1700
+ reason: `Payment verification ${paymentResult.status === "succeeded" ? "succeeded" : "resulted in status: " + newStatus}`,
1701
+ metadata: { paymentResult: paymentResult.metadata }
1702
+ }
1703
+ );
1704
+ transaction.status = newStatus;
1705
+ transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1706
+ transaction.verifiedBy = verifiedBy;
1707
+ transaction.gateway = {
1708
+ ...transaction.gateway,
1709
+ type: transaction.gateway?.type ?? "manual",
1710
+ verificationData: paymentResult.metadata
1711
+ };
1712
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1713
+ await transaction.save();
1714
+ if (newStatus === "verified") {
1715
+ this.events.emit("payment.verified", {
1716
+ transaction,
1717
+ paymentResult,
1718
+ verifiedBy: verifiedBy || void 0
1719
+ });
1720
+ } else if (newStatus === "failed") {
1721
+ this.events.emit("payment.failed", {
1722
+ transaction,
1723
+ error: paymentResult.metadata?.errorMessage || "Payment verification failed",
1724
+ provider: gatewayType,
1725
+ paymentIntentId: transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId
1726
+ });
1727
+ } else if (newStatus === "requires_action") {
1728
+ this.events.emit("payment.requires_action", {
1729
+ transaction,
1730
+ paymentResult,
1731
+ action: paymentResult.metadata?.requiredAction
1732
+ });
1733
+ } else if (newStatus === "processing") {
1734
+ this.events.emit("payment.processing", {
1735
+ transaction,
1736
+ paymentResult
1737
+ });
1738
+ }
1739
+ const result = {
1740
+ transaction,
1741
+ paymentResult,
1742
+ status: transaction.status
1743
+ };
1744
+ return this.plugins.executeHook(
1745
+ "payment.verify.after",
1746
+ this.getPluginContext(),
1747
+ { id: paymentIntentId, ...options },
1748
+ async () => result
1749
+ );
1750
+ }
1751
+ );
1752
+ }
1753
+ /**
1754
+ * Get payment status
1755
+ *
1756
+ * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
1757
+ * @returns { transaction, status }
1758
+ */
1759
+ async getStatus(paymentIntentId) {
1760
+ const TransactionModel = this.models.Transaction;
1761
+ const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1762
+ if (!transaction) {
1763
+ throw new TransactionNotFoundError(paymentIntentId);
1764
+ }
1765
+ const gatewayType = transaction.gateway?.type ?? "manual";
1766
+ const provider = this.providers[gatewayType];
1767
+ if (!provider) {
1768
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1769
+ }
1770
+ let paymentResult = null;
1771
+ try {
1772
+ const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
1773
+ paymentResult = await this.executeProviderCall(
1774
+ () => provider.getStatus(actualIntentId),
1775
+ `${gatewayType}.getStatus`
1776
+ );
1777
+ } catch (error) {
1778
+ this.logger.warn("Failed to get payment status from provider:", error);
1779
+ return {
1780
+ transaction,
1781
+ status: transaction.status,
1782
+ provider: gatewayType
1783
+ };
1784
+ }
1785
+ return {
1786
+ transaction,
1787
+ paymentResult,
1788
+ status: paymentResult.status,
1789
+ provider: gatewayType
1790
+ };
1791
+ }
1792
+ /**
1793
+ * Refund a payment
1794
+ *
1795
+ * @param paymentId - Payment intent ID, session ID, or transaction ID
1796
+ * @param amount - Amount to refund (optional, full refund if not provided)
1797
+ * @param options - Refund options
1798
+ * @returns { transaction, refundResult }
1799
+ */
1800
+ async refund(paymentId, amount = null, options = {}) {
1801
+ return this.plugins.executeHook(
1802
+ "payment.refund.before",
1803
+ this.getPluginContext(),
1804
+ { transactionId: paymentId, amount, ...options },
1805
+ async () => {
1806
+ const { reason = null } = options;
1807
+ const TransactionModel = this.models.Transaction;
1808
+ const transaction = await this._findTransaction(TransactionModel, paymentId);
1809
+ if (!transaction) {
1810
+ throw new TransactionNotFoundError(paymentId);
1811
+ }
1812
+ if (transaction.status !== "verified" && transaction.status !== "completed" && transaction.status !== "partially_refunded") {
1813
+ throw new InvalidStateTransitionError(
1814
+ "transaction",
1815
+ transaction._id.toString(),
1816
+ transaction.status,
1817
+ "verified, completed, or partially_refunded"
1818
+ );
1819
+ }
1820
+ const gatewayType = transaction.gateway?.type ?? "manual";
1821
+ const provider = this.providers[gatewayType];
1822
+ if (!provider) {
1823
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1824
+ }
1825
+ const capabilities = provider.getCapabilities();
1826
+ if (!capabilities.supportsRefunds) {
1827
+ throw new RefundNotSupportedError(gatewayType);
1828
+ }
1829
+ const refundedSoFar = transaction.refundedAmount ?? 0;
1830
+ const refundableAmount = transaction.amount - refundedSoFar;
1831
+ const refundAmount = amount ?? refundableAmount;
1832
+ if (refundAmount <= 0) {
1833
+ throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1834
+ }
1835
+ if (refundAmount > refundableAmount) {
1836
+ throw new ValidationError(
1837
+ `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
1838
+ { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
1839
+ );
1840
+ }
1841
+ let refundResult;
1842
+ try {
1843
+ const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentId;
1844
+ refundResult = await this.executeProviderCall(
1845
+ () => provider.refund(actualIntentId, refundAmount, { reason: reason ?? void 0 }),
1846
+ `${gatewayType}.refund`
1847
+ );
1848
+ } catch (error) {
1849
+ this.logger.error("Refund failed:", error);
1850
+ throw new RefundError(paymentId, error.message);
1851
+ }
1852
+ const refundFlow = this.config.transactionTypeMapping?.refund ?? TRANSACTION_FLOW.OUTFLOW;
1853
+ const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1854
+ let refundTaxAmount = 0;
1855
+ if (transaction.tax && transaction.tax > 0) {
1856
+ if (transaction.amount > 0) {
1857
+ const ratio = refundAmount / transaction.amount;
1858
+ refundTaxAmount = Math.round(transaction.tax * ratio);
1859
+ }
1860
+ }
1861
+ const refundFeeAmount = refundCommission?.gatewayFeeAmount || 0;
1862
+ const refundNetAmount = refundAmount - refundFeeAmount - refundTaxAmount;
1863
+ const refundTransaction = await TransactionModel.create({
1864
+ organizationId: transaction.organizationId,
1865
+ customerId: transaction.customerId,
1866
+ // ✅ UNIFIED: Type = category (semantic), Flow = direction (from config)
1867
+ type: "refund",
1868
+ // Category: this is a refund transaction
1869
+ flow: refundFlow,
1870
+ // Direction: income or expense (from config)
1871
+ tags: ["refund"],
1872
+ // ✅ UNIFIED: Amount structure
1873
+ amount: refundAmount,
1874
+ currency: transaction.currency,
1875
+ fee: refundFeeAmount,
1876
+ tax: refundTaxAmount,
1877
+ net: refundNetAmount,
1878
+ // Tax details (if tax existed on original)
1879
+ ...transaction.taxDetails && {
1880
+ taxDetails: transaction.taxDetails
1881
+ },
1882
+ method: transaction.method ?? "manual",
1883
+ status: "completed",
1884
+ gateway: {
1885
+ provider: transaction.gateway?.provider ?? "manual",
1886
+ paymentIntentId: refundResult.id,
1887
+ chargeId: refundResult.id
1888
+ },
1889
+ paymentDetails: transaction.paymentDetails,
1890
+ // Reversed commission
1891
+ ...refundCommission && { commission: refundCommission },
1892
+ // ✅ UNIFIED: Source reference + related transaction
1893
+ ...transaction.sourceId && { sourceId: transaction.sourceId },
1894
+ ...transaction.sourceModel && { sourceModel: transaction.sourceModel },
1895
+ relatedTransactionId: transaction._id,
1896
+ // Link to original transaction
1897
+ metadata: {
1898
+ ...transaction.metadata,
1899
+ isRefund: true,
1900
+ originalTransactionId: transaction._id.toString(),
1901
+ refundReason: reason,
1902
+ refundResult: refundResult.metadata
1903
+ },
1904
+ idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1905
+ });
1906
+ const isPartialRefund = refundAmount < refundableAmount;
1907
+ const refundStatus = isPartialRefund ? "partially_refunded" : "refunded";
1908
+ const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
1909
+ transaction.status,
1910
+ refundStatus,
1911
+ transaction._id.toString(),
1912
+ {
1913
+ changedBy: "system",
1914
+ reason: `Refund processed: ${isPartialRefund ? "partial" : "full"} refund of ${refundAmount}${reason ? " - " + reason : ""}`,
1915
+ metadata: {
1916
+ refundAmount,
1917
+ isPartialRefund,
1918
+ refundTransactionId: refundTransaction._id.toString()
1919
+ }
1920
+ }
1921
+ );
1922
+ transaction.status = refundStatus;
1923
+ transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1924
+ transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1925
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
1926
+ transaction.metadata = {
1927
+ ...transaction.metadata,
1928
+ refundTransactionId: refundTransaction._id.toString(),
1929
+ refundReason: reason
1930
+ };
1931
+ await transaction.save();
1932
+ this.events.emit("payment.refunded", {
1933
+ transaction,
1934
+ refundTransaction,
1935
+ refundResult: {
1936
+ ...refundResult,
1937
+ currency: refundResult.currency ?? "USD",
1938
+ metadata: refundResult.metadata ?? {}
1939
+ },
1940
+ refundAmount,
1941
+ reason: reason ?? void 0,
1942
+ isPartialRefund
1943
+ });
1944
+ const result = {
1945
+ transaction,
1946
+ refundTransaction,
1947
+ refundResult,
1948
+ status: transaction.status
1949
+ };
1950
+ return this.plugins.executeHook(
1951
+ "payment.refund.after",
1952
+ this.getPluginContext(),
1953
+ { transactionId: paymentId, amount, ...options },
1954
+ async () => result
1955
+ );
1956
+ }
1957
+ );
1958
+ }
1959
+ /**
1960
+ * Handle webhook from payment provider
1961
+ *
1962
+ * @param provider - Provider name
1963
+ * @param payload - Webhook payload
1964
+ * @param headers - Request headers
1965
+ * @returns { event, transaction }
1966
+ */
1967
+ async handleWebhook(providerName, payload, headers = {}) {
1968
+ const provider = this.providers[providerName];
1969
+ if (!provider) {
1970
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1971
+ }
1972
+ const capabilities = provider.getCapabilities();
1973
+ if (!capabilities.supportsWebhooks) {
1974
+ throw new ProviderCapabilityError(providerName, "webhooks");
1975
+ }
1976
+ let webhookEvent;
1977
+ try {
1978
+ webhookEvent = await this.executeProviderCall(
1979
+ () => provider.handleWebhook(payload, headers),
1980
+ `${providerName}.handleWebhook`
1981
+ );
1982
+ } catch (error) {
1983
+ this.logger.error("Webhook processing failed:", error);
1984
+ throw new ProviderError(
1985
+ `Webhook processing failed for ${providerName}: ${error.message}`,
1986
+ "WEBHOOK_PROCESSING_FAILED",
1987
+ { retryable: false }
1988
+ );
1989
+ }
1990
+ if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) {
1991
+ throw new ValidationError(
1992
+ `Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`,
1993
+ { provider: providerName, eventType: webhookEvent?.type }
1994
+ );
1995
+ }
1996
+ const TransactionModel = this.models.Transaction;
1997
+ let transaction = null;
1998
+ if (webhookEvent.data.sessionId) {
1999
+ transaction = await TransactionModel.findOne({
2000
+ "gateway.sessionId": webhookEvent.data.sessionId
2001
+ });
2002
+ }
2003
+ if (!transaction && webhookEvent.data.paymentIntentId) {
2004
+ transaction = await TransactionModel.findOne({
2005
+ "gateway.paymentIntentId": webhookEvent.data.paymentIntentId
2006
+ });
2007
+ }
2008
+ if (!transaction) {
2009
+ this.logger.warn("Transaction not found for webhook event", {
2010
+ provider: providerName,
2011
+ eventId: webhookEvent.id,
2012
+ sessionId: webhookEvent.data.sessionId,
2013
+ paymentIntentId: webhookEvent.data.paymentIntentId
2014
+ });
2015
+ throw new TransactionNotFoundError(
2016
+ webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown"
2017
+ );
2018
+ }
2019
+ if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) {
2020
+ transaction.gateway = {
2021
+ ...transaction.gateway,
2022
+ type: transaction.gateway?.type ?? "manual",
2023
+ sessionId: webhookEvent.data.sessionId
2024
+ };
2025
+ }
2026
+ if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) {
2027
+ transaction.gateway = {
2028
+ ...transaction.gateway,
2029
+ type: transaction.gateway?.type ?? "manual",
2030
+ paymentIntentId: webhookEvent.data.paymentIntentId
2031
+ };
2032
+ }
2033
+ if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
2034
+ this.logger.warn("Webhook already processed", {
2035
+ transactionId: transaction._id,
2036
+ eventId: webhookEvent.id
2037
+ });
2038
+ return {
2039
+ event: webhookEvent,
2040
+ transaction,
2041
+ status: "already_processed"
2042
+ };
2043
+ }
2044
+ transaction.webhook = {
2045
+ eventId: webhookEvent.id,
2046
+ eventType: webhookEvent.type,
2047
+ receivedAt: /* @__PURE__ */ new Date(),
2048
+ processedAt: /* @__PURE__ */ new Date(),
2049
+ data: webhookEvent.data
2050
+ };
2051
+ let newStatus = transaction.status;
2052
+ if (webhookEvent.type === "payment.succeeded") {
2053
+ newStatus = "verified";
2054
+ } else if (webhookEvent.type === "payment.failed") {
2055
+ newStatus = "failed";
2056
+ } else if (webhookEvent.type === "refund.succeeded") {
2057
+ newStatus = "refunded";
2058
+ }
2059
+ if (newStatus !== transaction.status) {
2060
+ const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2061
+ transaction.status,
2062
+ newStatus,
2063
+ transaction._id.toString(),
2064
+ {
2065
+ changedBy: "webhook",
2066
+ reason: `Webhook event: ${webhookEvent.type}`,
2067
+ metadata: {
2068
+ webhookId: webhookEvent.id,
2069
+ webhookType: webhookEvent.type,
2070
+ webhookData: webhookEvent.data
2071
+ }
2072
+ }
2073
+ );
2074
+ transaction.status = newStatus;
2075
+ if (newStatus === "verified") {
2076
+ transaction.verifiedAt = webhookEvent.createdAt;
2077
+ } else if (newStatus === "refunded") {
2078
+ transaction.refundedAt = webhookEvent.createdAt;
2079
+ } else if (newStatus === "failed") {
2080
+ transaction.failedAt = webhookEvent.createdAt;
2081
+ }
2082
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
2083
+ }
2084
+ await transaction.save();
2085
+ this.events.emit("webhook.processed", {
2086
+ webhookType: webhookEvent.type,
2087
+ provider: webhookEvent.provider,
2088
+ event: webhookEvent,
2089
+ transaction,
2090
+ processedAt: /* @__PURE__ */ new Date()
2091
+ });
2092
+ return {
2093
+ event: webhookEvent,
2094
+ transaction,
2095
+ status: "processed"
2096
+ };
2097
+ }
2098
+ /**
2099
+ * List payments/transactions with filters
2100
+ *
2101
+ * @param filters - Query filters
2102
+ * @param options - Query options (limit, skip, sort)
2103
+ * @returns Transactions
2104
+ */
2105
+ async list(filters = {}, options = {}) {
2106
+ const TransactionModel = this.models.Transaction;
2107
+ const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
2108
+ const transactions = await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
2109
+ return transactions;
2110
+ }
2111
+ /**
2112
+ * Get payment/transaction by ID
2113
+ *
2114
+ * @param transactionId - Transaction ID
2115
+ * @returns Transaction
2116
+ */
2117
+ async get(transactionId) {
2118
+ const TransactionModel = this.models.Transaction;
2119
+ const transaction = await TransactionModel.findById(transactionId);
2120
+ if (!transaction) {
2121
+ throw new TransactionNotFoundError(transactionId);
2122
+ }
2123
+ return transaction;
2124
+ }
2125
+ /**
2126
+ * Get provider instance
2127
+ *
2128
+ * @param providerName - Provider name
2129
+ * @returns Provider instance
2130
+ */
2131
+ getProvider(providerName) {
2132
+ const provider = this.providers[providerName];
2133
+ if (!provider) {
2134
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
2135
+ }
2136
+ return provider;
2137
+ }
2138
+ /**
2139
+ * Find transaction by sessionId, paymentIntentId, or transaction ID
2140
+ * @private
2141
+ */
2142
+ async _findTransaction(TransactionModel, identifier) {
2143
+ let transaction = await TransactionModel.findOne({
2144
+ "gateway.sessionId": identifier
2145
+ });
2146
+ if (!transaction) {
2147
+ transaction = await TransactionModel.findOne({
2148
+ "gateway.paymentIntentId": identifier
2149
+ });
2150
+ }
2151
+ if (!transaction) {
2152
+ transaction = await TransactionModel.findById(identifier);
2153
+ }
2154
+ return transaction;
2155
+ }
2156
+ };
2157
+ var TransactionService = class {
2158
+ models;
2159
+ plugins;
2160
+ events;
2161
+ logger;
2162
+ constructor(container) {
2163
+ this.models = container.get("models");
2164
+ this.plugins = container.get("plugins");
2165
+ this.events = container.get("events");
2166
+ this.logger = container.get("logger");
2167
+ }
2168
+ /**
2169
+ * Create plugin context for hook execution
2170
+ * @private
2171
+ */
2172
+ getPluginContext() {
2173
+ return {
2174
+ events: this.events,
2175
+ logger: this.logger,
2176
+ storage: /* @__PURE__ */ new Map(),
2177
+ meta: {
2178
+ requestId: nanoid(),
2179
+ timestamp: /* @__PURE__ */ new Date()
2180
+ }
2181
+ };
2182
+ }
2183
+ /**
2184
+ * Get transaction by ID
2185
+ *
2186
+ * @param transactionId - Transaction ID
2187
+ * @returns Transaction
2188
+ */
2189
+ async get(transactionId) {
2190
+ const TransactionModel = this.models.Transaction;
2191
+ const transaction = await TransactionModel.findById(transactionId);
2192
+ if (!transaction) {
2193
+ throw new TransactionNotFoundError(transactionId);
2194
+ }
2195
+ return transaction;
2196
+ }
2197
+ /**
2198
+ * List transactions with filters
2199
+ *
2200
+ * @param filters - Query filters
2201
+ * @param options - Query options (limit, skip, sort, populate)
2202
+ * @returns { transactions, total, page, limit }
2203
+ */
2204
+ async list(filters = {}, options = {}) {
2205
+ const TransactionModel = this.models.Transaction;
2206
+ const {
2207
+ limit = 50,
2208
+ skip = 0,
2209
+ page = null,
2210
+ sort = { createdAt: -1 },
2211
+ populate = []
2212
+ } = options;
2213
+ const actualSkip = page ? (page - 1) * limit : skip;
2214
+ let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
2215
+ if (populate.length > 0 && typeof query.populate === "function") {
2216
+ populate.forEach((field) => {
2217
+ query = query.populate(field);
2218
+ });
2219
+ }
2220
+ const transactions = await query;
2221
+ const model = TransactionModel;
2222
+ const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
2223
+ return {
2224
+ transactions,
2225
+ total,
2226
+ page: page ?? Math.floor(actualSkip / limit) + 1,
2227
+ limit,
2228
+ pages: Math.ceil(total / limit)
2229
+ };
2230
+ }
2231
+ /**
2232
+ * Update transaction
2233
+ *
2234
+ * @param transactionId - Transaction ID
2235
+ * @param updates - Fields to update
2236
+ * @returns Updated transaction
2237
+ */
2238
+ async update(transactionId, updates) {
2239
+ const hookInput = { transactionId, updates };
2240
+ return this.plugins.executeHook(
2241
+ "transaction.update.before",
2242
+ this.getPluginContext(),
2243
+ hookInput,
2244
+ async () => {
2245
+ const TransactionModel = this.models.Transaction;
2246
+ const effectiveUpdates = hookInput.updates;
2247
+ const model = TransactionModel;
2248
+ let transaction;
2249
+ if (typeof model.update === "function") {
2250
+ transaction = await model.update(transactionId, effectiveUpdates);
2251
+ } else if (typeof model.findByIdAndUpdate === "function") {
2252
+ transaction = await model.findByIdAndUpdate(
2253
+ transactionId,
2254
+ { $set: effectiveUpdates },
2255
+ { new: true }
2256
+ );
2257
+ } else {
2258
+ throw new Error("Transaction model does not support update operations");
2259
+ }
2260
+ if (!transaction) {
2261
+ throw new TransactionNotFoundError(transactionId);
2262
+ }
2263
+ this.events.emit("transaction.updated", {
2264
+ transaction,
2265
+ updates: effectiveUpdates
2266
+ });
2267
+ return this.plugins.executeHook(
2268
+ "transaction.update.after",
2269
+ this.getPluginContext(),
2270
+ { transactionId, updates: effectiveUpdates },
2271
+ async () => transaction
2272
+ );
2273
+ }
2274
+ );
2275
+ }
2276
+ };
2277
+
2278
+ // src/shared/utils/calculators/commission-split.ts
2279
+ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2280
+ if (!splitRules || splitRules.length === 0) {
2281
+ return [];
2282
+ }
2283
+ if (amount < 0) {
2284
+ throw new Error("Transaction amount cannot be negative");
2285
+ }
2286
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
2287
+ throw new Error("Gateway fee rate must be between 0 and 1");
2288
+ }
2289
+ const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
2290
+ if (totalRate > 1) {
2291
+ throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
2292
+ }
2293
+ return splitRules.map((rule, index) => {
2294
+ if (rule.rate < 0 || rule.rate > 1) {
2295
+ throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
2296
+ }
2297
+ const grossAmount = Math.round(amount * rule.rate);
2298
+ const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate) : 0;
2299
+ const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
2300
+ return {
2301
+ type: rule.type ?? SPLIT_TYPE.CUSTOM,
2302
+ recipientId: rule.recipientId,
2303
+ recipientType: rule.recipientType,
2304
+ rate: rule.rate,
2305
+ grossAmount,
2306
+ gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
2307
+ gatewayFeeAmount,
2308
+ netAmount,
2309
+ status: SPLIT_STATUS.PENDING,
2310
+ dueDate: rule.dueDate ?? null,
2311
+ metadata: rule.metadata ?? {}
2312
+ };
2313
+ });
2314
+ }
2315
+ function calculateOrganizationPayout(amount, splits = []) {
2316
+ const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
2317
+ return Math.max(0, amount - totalSplitAmount);
2318
+ }
2319
+
2320
+ // src/application/services/escrow.service.ts
2321
+ var EscrowService = class {
2322
+ models;
2323
+ plugins;
2324
+ logger;
2325
+ events;
2326
+ constructor(container) {
2327
+ this.models = container.get("models");
2328
+ this.plugins = container.get("plugins");
2329
+ this.logger = container.get("logger");
2330
+ this.events = container.get("events");
2331
+ }
2332
+ /**
2333
+ * Create plugin context for hook execution
2334
+ * @private
2335
+ */
2336
+ getPluginContext() {
2337
+ return {
2338
+ events: this.events,
2339
+ logger: this.logger,
2340
+ storage: /* @__PURE__ */ new Map(),
2341
+ meta: {
2342
+ requestId: nanoid(),
2343
+ timestamp: /* @__PURE__ */ new Date()
2344
+ }
2345
+ };
2346
+ }
2347
+ /**
2348
+ * Hold funds in escrow
2349
+ *
2350
+ * @param transactionId - Transaction to hold
2351
+ * @param options - Hold options
2352
+ * @returns Updated transaction
2353
+ */
2354
+ async hold(transactionId, options = {}) {
2355
+ return this.plugins.executeHook(
2356
+ "escrow.hold.before",
2357
+ this.getPluginContext(),
2358
+ { transactionId, ...options },
2359
+ async () => {
2360
+ const {
2361
+ reason = HOLD_REASON.PAYMENT_VERIFICATION,
2362
+ holdUntil = null,
2363
+ metadata = {}
2364
+ } = options;
2365
+ const TransactionModel = this.models.Transaction;
2366
+ const transaction = await TransactionModel.findById(transactionId);
2367
+ if (!transaction) {
2368
+ throw new TransactionNotFoundError(transactionId);
2369
+ }
2370
+ if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
2371
+ throw new InvalidStateTransitionError(
2372
+ "transaction",
2373
+ transaction._id.toString(),
2374
+ transaction.status,
2375
+ TRANSACTION_STATUS.VERIFIED
2376
+ );
2377
+ }
2378
+ const heldAmount = transaction.amount;
2379
+ transaction.hold = {
2380
+ status: HOLD_STATUS.HELD,
2381
+ heldAmount,
2382
+ releasedAmount: 0,
2383
+ reason,
2384
+ heldAt: /* @__PURE__ */ new Date(),
2385
+ ...holdUntil && { holdUntil },
2386
+ releases: [],
2387
+ metadata
2388
+ };
2389
+ await transaction.save();
2390
+ this.events.emit("escrow.held", {
2391
+ transaction,
2392
+ heldAmount,
2393
+ reason
2394
+ });
2395
+ return this.plugins.executeHook(
2396
+ "escrow.hold.after",
2397
+ this.getPluginContext(),
2398
+ { transactionId, ...options },
2399
+ async () => transaction
2400
+ );
2401
+ }
2402
+ );
2403
+ }
2404
+ /**
2405
+ * Release funds from escrow to recipient
2406
+ *
2407
+ * @param transactionId - Transaction to release
2408
+ * @param options - Release options
2409
+ * @returns { transaction, releaseTransaction }
2410
+ */
2411
+ async release(transactionId, options) {
2412
+ return this.plugins.executeHook(
2413
+ "escrow.release.before",
2414
+ this.getPluginContext(),
2415
+ { transactionId, ...options },
2416
+ async () => {
2417
+ const {
2418
+ amount = null,
2419
+ recipientId,
2420
+ recipientType = "organization",
2421
+ reason = RELEASE_REASON.PAYMENT_VERIFIED,
2422
+ releasedBy = null,
2423
+ createTransaction = true,
2424
+ metadata = {}
2425
+ } = options;
2426
+ const TransactionModel = this.models.Transaction;
2427
+ const transaction = await TransactionModel.findById(transactionId);
2428
+ if (!transaction) {
2429
+ throw new TransactionNotFoundError(transactionId);
2430
+ }
2431
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2432
+ throw new InvalidStateTransitionError(
2433
+ "escrow_hold",
2434
+ transaction._id.toString(),
2435
+ transaction.hold?.status ?? "none",
2436
+ HOLD_STATUS.HELD
2437
+ );
2438
+ }
2439
+ if (!recipientId) {
2440
+ throw new ValidationError("recipientId is required for release", { transactionId });
2441
+ }
2442
+ const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
2443
+ const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
2444
+ if (releaseAmount > availableAmount) {
2445
+ throw new ValidationError(
2446
+ `Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`,
2447
+ { releaseAmount, availableAmount, transactionId }
2448
+ );
2449
+ }
2450
+ const releaseRecord = {
2451
+ amount: releaseAmount,
2452
+ recipientId,
2453
+ recipientType,
2454
+ releasedAt: /* @__PURE__ */ new Date(),
2455
+ releasedBy,
2456
+ reason,
2457
+ metadata
2458
+ };
2459
+ transaction.hold.releases.push(releaseRecord);
2460
+ transaction.hold.releasedAmount += releaseAmount;
2461
+ const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
2462
+ const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
2463
+ if (isFullRelease) {
2464
+ const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
2465
+ transaction.hold.status,
2466
+ HOLD_STATUS.RELEASED,
2467
+ transaction._id.toString(),
2468
+ {
2469
+ changedBy: releasedBy ?? "system",
2470
+ reason: `Escrow hold fully released: ${releaseAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
2471
+ metadata: { releaseAmount, recipientId, releaseReason: reason }
2472
+ }
2473
+ );
2474
+ transaction.hold.status = HOLD_STATUS.RELEASED;
2475
+ transaction.hold.releasedAt = /* @__PURE__ */ new Date();
2476
+ const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2477
+ transaction.status,
2478
+ TRANSACTION_STATUS.COMPLETED,
2479
+ transaction._id.toString(),
2480
+ {
2481
+ changedBy: releasedBy ?? "system",
2482
+ reason: `Transaction completed after full escrow release`,
2483
+ metadata: { releaseAmount, recipientId }
2484
+ }
2485
+ );
2486
+ transaction.status = TRANSACTION_STATUS.COMPLETED;
2487
+ Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
2488
+ Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
2489
+ } else if (isPartialRelease) {
2490
+ const auditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
2491
+ transaction.hold.status,
2492
+ HOLD_STATUS.PARTIALLY_RELEASED,
2493
+ transaction._id.toString(),
2494
+ {
2495
+ changedBy: releasedBy ?? "system",
2496
+ reason: `Partial escrow release: ${releaseAmount} of ${transaction.hold.heldAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
2497
+ metadata: {
2498
+ releaseAmount,
2499
+ recipientId,
2500
+ releaseReason: reason,
2501
+ remainingHeld: transaction.hold.heldAmount - transaction.hold.releasedAmount
2502
+ }
2503
+ }
2504
+ );
2505
+ transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
2506
+ Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
2507
+ }
2508
+ if ("markModified" in transaction) {
2509
+ transaction.markModified("hold");
2510
+ }
2511
+ await transaction.save();
2512
+ let releaseTaxAmount = 0;
2513
+ if (transaction.tax && transaction.tax > 0) {
2514
+ if (releaseAmount === availableAmount && !amount) {
2515
+ const releasedTaxSoFar = transaction.hold.releasedTaxAmount ?? 0;
2516
+ releaseTaxAmount = transaction.tax - releasedTaxSoFar;
2517
+ } else {
2518
+ const totalAmount = transaction.amount + transaction.tax;
2519
+ if (totalAmount > 0) {
2520
+ const taxRatio = transaction.tax / totalAmount;
2521
+ releaseTaxAmount = Math.round(releaseAmount * taxRatio);
2522
+ }
2523
+ }
2524
+ }
2525
+ const releaseNetAmount = releaseAmount - releaseTaxAmount;
2526
+ let releaseTransaction = null;
2527
+ if (createTransaction) {
2528
+ releaseTransaction = await TransactionModel.create({
2529
+ organizationId: transaction.organizationId,
2530
+ customerId: recipientId,
2531
+ // ✅ UNIFIED: Use 'escrow_release' as type, inflow
2532
+ type: "escrow_release",
2533
+ flow: "inflow",
2534
+ tags: ["escrow", "release"],
2535
+ // ✅ UNIFIED: Amount structure
2536
+ amount: releaseAmount,
2537
+ currency: transaction.currency,
2538
+ fee: 0,
2539
+ // No processing fees on releases
2540
+ tax: releaseTaxAmount,
2541
+ // ✅ Top-level number
2542
+ net: releaseNetAmount,
2543
+ // Copy tax details from original transaction
2544
+ ...transaction.taxDetails && {
2545
+ taxDetails: transaction.taxDetails
2546
+ },
2547
+ method: transaction.method,
2548
+ status: "completed",
2549
+ gateway: transaction.gateway,
2550
+ // ✅ UNIFIED: Source reference (link to held transaction)
2551
+ sourceId: transaction._id,
2552
+ sourceModel: "Transaction",
2553
+ relatedTransactionId: transaction._id,
2554
+ metadata: {
2555
+ ...metadata,
2556
+ isRelease: true,
2557
+ heldTransactionId: transaction._id.toString(),
2558
+ releaseReason: reason,
2559
+ recipientType,
2560
+ // Store original category for reference
2561
+ originalCategory: transaction.category
2562
+ },
2563
+ idempotencyKey: `release_${transaction._id}_${Date.now()}`
2564
+ });
2565
+ }
2566
+ this.events.emit("escrow.released", {
2567
+ transaction,
2568
+ releaseTransaction,
2569
+ releaseAmount,
2570
+ recipientId,
2571
+ recipientType,
2572
+ reason,
2573
+ isFullRelease,
2574
+ isPartialRelease
2575
+ });
2576
+ const result = {
2577
+ transaction,
2578
+ releaseTransaction,
2579
+ releaseAmount,
2580
+ isFullRelease,
2581
+ isPartialRelease
2582
+ };
2583
+ return this.plugins.executeHook(
2584
+ "escrow.release.after",
2585
+ this.getPluginContext(),
2586
+ { transactionId, ...options },
2587
+ async () => result
2588
+ );
2589
+ }
2590
+ );
2591
+ }
2592
+ /**
2593
+ * Cancel hold and release back to customer
2594
+ *
2595
+ * @param transactionId - Transaction to cancel hold
2596
+ * @param options - Cancel options
2597
+ * @returns Updated transaction
2598
+ */
2599
+ async cancel(transactionId, options = {}) {
2600
+ const { reason = "Hold cancelled", metadata = {} } = options;
2601
+ const TransactionModel = this.models.Transaction;
2602
+ const transaction = await TransactionModel.findById(transactionId);
2603
+ if (!transaction) {
2604
+ throw new TransactionNotFoundError(transactionId);
2605
+ }
2606
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2607
+ throw new InvalidStateTransitionError(
2608
+ "escrow_hold",
2609
+ transaction._id.toString(),
2610
+ transaction.hold?.status ?? "none",
2611
+ HOLD_STATUS.HELD
2612
+ );
2613
+ }
2614
+ const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
2615
+ transaction.hold.status,
2616
+ HOLD_STATUS.CANCELLED,
2617
+ transaction._id.toString(),
2618
+ {
2619
+ changedBy: "system",
2620
+ reason: `Escrow hold cancelled${reason ? ": " + reason : ""}`,
2621
+ metadata: { cancelReason: reason, ...metadata }
2622
+ }
2623
+ );
2624
+ transaction.hold.status = HOLD_STATUS.CANCELLED;
2625
+ transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
2626
+ transaction.hold.metadata = {
2627
+ ...transaction.hold.metadata,
2628
+ ...metadata,
2629
+ cancelReason: reason
2630
+ };
2631
+ const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
2632
+ transaction.status,
2633
+ TRANSACTION_STATUS.CANCELLED,
2634
+ transaction._id.toString(),
2635
+ {
2636
+ changedBy: "system",
2637
+ reason: `Transaction cancelled due to escrow hold cancellation`,
2638
+ metadata: { cancelReason: reason }
2639
+ }
2640
+ );
2641
+ transaction.status = TRANSACTION_STATUS.CANCELLED;
2642
+ Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
2643
+ Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
2644
+ if ("markModified" in transaction) {
2645
+ transaction.markModified("hold");
2646
+ }
2647
+ await transaction.save();
2648
+ this.events.emit("escrow.cancelled", {
2649
+ transaction,
2650
+ reason
2651
+ });
2652
+ return transaction;
2653
+ }
2654
+ /**
2655
+ * Split payment to multiple recipients
2656
+ * Deducts splits from held amount and releases remainder to organization
2657
+ *
2658
+ * @param transactionId - Transaction to split
2659
+ * @param splitRules - Split configuration
2660
+ * @returns { transaction, splitTransactions, organizationTransaction }
2661
+ */
2662
+ async split(transactionId, splitRules = []) {
2663
+ const TransactionModel = this.models.Transaction;
2664
+ const transaction = await TransactionModel.findById(transactionId);
2665
+ if (!transaction) {
2666
+ throw new TransactionNotFoundError(transactionId);
2667
+ }
2668
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2669
+ throw new InvalidStateTransitionError(
2670
+ "escrow_hold",
2671
+ transaction._id.toString(),
2672
+ transaction.hold?.status ?? "none",
2673
+ HOLD_STATUS.HELD
2674
+ );
2675
+ }
2676
+ if (!splitRules || splitRules.length === 0) {
2677
+ throw new ValidationError("splitRules cannot be empty", { transactionId });
2678
+ }
2679
+ const splits = calculateSplits(
2680
+ transaction.amount,
2681
+ splitRules,
2682
+ transaction.commission?.gatewayFeeRate ?? 0
2683
+ );
2684
+ transaction.splits = splits;
2685
+ await transaction.save();
2686
+ const splitTransactions = [];
2687
+ const totalTax = transaction.tax ?? 0;
2688
+ const totalBaseAmount = transaction.amount;
2689
+ let allocatedTaxAmount = 0;
2690
+ const splitTaxAmounts = splits.map((split) => {
2691
+ if (!totalTax || totalBaseAmount <= 0) {
2692
+ return 0;
2693
+ }
2694
+ const ratio = split.grossAmount / totalBaseAmount;
2695
+ const taxAmount = Math.round(totalTax * ratio);
2696
+ allocatedTaxAmount += taxAmount;
2697
+ return taxAmount;
2698
+ });
2699
+ for (const [index, split] of splits.entries()) {
2700
+ const splitTaxAmount = totalTax > 0 ? splitTaxAmounts[index] ?? 0 : 0;
2701
+ const splitNetAmount = split.grossAmount - split.gatewayFeeAmount - splitTaxAmount;
2702
+ const splitTransaction = await TransactionModel.create({
2703
+ organizationId: transaction.organizationId,
2704
+ customerId: split.recipientId,
2705
+ // ✅ UNIFIED: Use split type directly (commission, platform_fee, etc.)
2706
+ type: split.type,
2707
+ flow: "outflow",
2708
+ // Splits are money going out
2709
+ tags: ["split", "commission"],
2710
+ // ✅ UNIFIED: Amount structure (gross, fee, tax, net)
2711
+ amount: split.grossAmount,
2712
+ // ✅ Gross amount (before deductions)
2713
+ currency: transaction.currency,
2714
+ fee: split.gatewayFeeAmount,
2715
+ tax: splitTaxAmount,
2716
+ // ✅ Top-level number
2717
+ net: splitNetAmount,
2718
+ // ✅ Net = gross - fee - tax
2719
+ // Copy tax details from original transaction (if applicable)
2720
+ ...transaction.taxDetails && splitTaxAmount > 0 && {
2721
+ taxDetails: transaction.taxDetails
2722
+ },
2723
+ method: transaction.method,
2724
+ status: "completed",
2725
+ gateway: transaction.gateway,
2726
+ // ✅ UNIFIED: Source reference (link to original transaction)
2727
+ sourceId: transaction._id,
2728
+ sourceModel: "Transaction",
2729
+ relatedTransactionId: transaction._id,
2730
+ metadata: {
2731
+ isSplit: true,
2732
+ splitType: split.type,
2733
+ recipientType: split.recipientType,
2734
+ originalTransactionId: transaction._id.toString(),
2735
+ // Store split details for reference
2736
+ splitGrossAmount: split.grossAmount,
2737
+ splitNetAmount: split.netAmount,
2738
+ // Original calculation
2739
+ gatewayFeeAmount: split.gatewayFeeAmount
2740
+ },
2741
+ idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
2742
+ });
2743
+ split.payoutTransactionId = splitTransaction._id.toString();
2744
+ split.status = SPLIT_STATUS.PAID;
2745
+ split.paidDate = /* @__PURE__ */ new Date();
2746
+ splitTransactions.push(splitTransaction);
2747
+ }
2748
+ await transaction.save();
2749
+ const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
2750
+ const organizationTaxAmount = totalTax > 0 ? Math.max(0, totalTax - allocatedTaxAmount) : 0;
2751
+ const organizationPayoutTotal = totalTax > 0 ? organizationPayout + organizationTaxAmount : organizationPayout;
2752
+ const organizationTransaction = await this.release(transactionId, {
2753
+ amount: organizationPayoutTotal,
2754
+ recipientId: transaction.organizationId?.toString() ?? "",
2755
+ recipientType: "organization",
2756
+ reason: RELEASE_REASON.PAYMENT_VERIFIED,
2757
+ createTransaction: true,
2758
+ metadata: {
2759
+ afterSplits: true,
2760
+ totalSplits: splits.length,
2761
+ totalSplitAmount: transaction.amount - organizationPayout
2762
+ }
2763
+ });
2764
+ this.events.emit("escrow.split", {
2765
+ transaction,
2766
+ splits,
2767
+ splitTransactions,
2768
+ organizationTransaction: organizationTransaction.releaseTransaction,
2769
+ organizationPayout
2770
+ });
2771
+ return {
2772
+ transaction,
2773
+ splits,
2774
+ splitTransactions,
2775
+ organizationTransaction: organizationTransaction.releaseTransaction,
2776
+ organizationPayout
2777
+ };
2778
+ }
2779
+ /**
2780
+ * Get escrow status
2781
+ *
2782
+ * @param transactionId - Transaction ID
2783
+ * @returns Escrow status
2784
+ */
2785
+ async getStatus(transactionId) {
2786
+ const TransactionModel = this.models.Transaction;
2787
+ const transaction = await TransactionModel.findById(transactionId);
2788
+ if (!transaction) {
2789
+ throw new TransactionNotFoundError(transactionId);
2790
+ }
2791
+ return {
2792
+ transaction,
2793
+ hold: transaction.hold ?? null,
2794
+ splits: transaction.splits ?? [],
2795
+ hasHold: !!transaction.hold,
2796
+ hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2797
+ };
2798
+ }
2799
+ };
2800
+
2801
+ // src/application/services/settlement.service.ts
2802
+ var SettlementService = class {
2803
+ models;
2804
+ plugins;
2805
+ logger;
2806
+ events;
2807
+ constructor(container) {
2808
+ this.models = container.get("models");
2809
+ this.plugins = container.get("plugins");
2810
+ this.logger = container.get("logger");
2811
+ this.events = container.get("events");
2812
+ void this.plugins;
2813
+ }
2814
+ /**
2815
+ * Create settlements from transaction splits
2816
+ * Typically called after escrow is released
2817
+ *
2818
+ * @param transactionId - Transaction ID with splits
2819
+ * @param options - Creation options
2820
+ * @returns Array of created settlements
2821
+ */
2822
+ async createFromSplits(transactionId, options = {}) {
2823
+ const {
2824
+ scheduledAt = /* @__PURE__ */ new Date(),
2825
+ payoutMethod = "bank_transfer",
2826
+ metadata = {}
2827
+ } = options;
2828
+ if (!this.models.Settlement) {
2829
+ throw new ModelNotRegisteredError("Settlement");
2830
+ }
2831
+ const TransactionModel = this.models.Transaction;
2832
+ const transaction = await TransactionModel.findById(transactionId);
2833
+ if (!transaction) {
2834
+ throw new TransactionNotFoundError(transactionId);
2835
+ }
2836
+ if (!transaction.splits || transaction.splits.length === 0) {
2837
+ throw new ValidationError("Transaction has no splits to settle", { transactionId });
2838
+ }
2839
+ const SettlementModel = this.models.Settlement;
2840
+ const settlements = [];
2841
+ for (const split of transaction.splits) {
2842
+ if (split.status === "paid") {
2843
+ this.logger.info("Split already paid, skipping", { splitId: split._id });
2844
+ continue;
2845
+ }
2846
+ const settlement = await SettlementModel.create({
2847
+ organizationId: transaction.organizationId,
2848
+ recipientId: split.recipientId,
2849
+ recipientType: split.recipientType,
2850
+ type: SETTLEMENT_TYPE.SPLIT_PAYOUT,
2851
+ status: SETTLEMENT_STATUS.PENDING,
2852
+ payoutMethod,
2853
+ amount: split.netAmount,
2854
+ currency: transaction.currency,
2855
+ sourceTransactionIds: [transaction._id],
2856
+ sourceSplitIds: [split._id?.toString() || ""],
2857
+ scheduledAt,
2858
+ metadata: {
2859
+ ...metadata,
2860
+ splitType: split.type,
2861
+ transactionCategory: transaction.category
2862
+ }
2863
+ });
2864
+ settlements.push(settlement);
2865
+ }
2866
+ this.events.emit("settlement.created", {
2867
+ settlements,
2868
+ transactionId,
2869
+ count: settlements.length
2870
+ });
2871
+ this.logger.info("Created settlements from splits", {
2872
+ transactionId,
2873
+ count: settlements.length
2874
+ });
2875
+ return settlements;
2876
+ }
2877
+ /**
2878
+ * Schedule a payout
2879
+ *
2880
+ * @param params - Settlement parameters
2881
+ * @returns Created settlement
2882
+ */
2883
+ async schedule(params) {
2884
+ if (!this.models.Settlement) {
2885
+ throw new ModelNotRegisteredError("Settlement");
2886
+ }
2887
+ const {
2888
+ organizationId,
2889
+ recipientId,
2890
+ recipientType,
2891
+ type,
2892
+ amount,
2893
+ currency = "USD",
2894
+ payoutMethod,
2895
+ sourceTransactionIds = [],
2896
+ sourceSplitIds = [],
2897
+ scheduledAt = /* @__PURE__ */ new Date(),
2898
+ bankTransferDetails,
2899
+ mobileWalletDetails,
2900
+ cryptoDetails,
2901
+ notes,
2902
+ metadata = {}
2903
+ } = params;
2904
+ if (amount <= 0) {
2905
+ throw new ValidationError("Settlement amount must be positive", { amount });
2906
+ }
2907
+ const SettlementModel = this.models.Settlement;
2908
+ const settlement = await SettlementModel.create({
2909
+ organizationId,
2910
+ recipientId,
2911
+ recipientType,
2912
+ type,
2913
+ status: SETTLEMENT_STATUS.PENDING,
2914
+ payoutMethod,
2915
+ amount,
2916
+ currency,
2917
+ sourceTransactionIds,
2918
+ sourceSplitIds,
2919
+ scheduledAt,
2920
+ bankTransferDetails,
2921
+ mobileWalletDetails,
2922
+ cryptoDetails,
2923
+ notes,
2924
+ metadata
2925
+ });
2926
+ this.events.emit("settlement.scheduled", {
2927
+ settlement,
2928
+ scheduledAt
2929
+ });
2930
+ this.logger.info("Settlement scheduled", {
2931
+ settlementId: settlement._id,
2932
+ recipientId,
2933
+ amount
2934
+ });
2935
+ return settlement;
2936
+ }
2937
+ /**
2938
+ * Process pending settlements
2939
+ * Batch process settlements that are due
2940
+ *
2941
+ * @param options - Processing options
2942
+ * @returns Processing result
2943
+ */
2944
+ async processPending(options = {}) {
2945
+ if (!this.models.Settlement) {
2946
+ throw new ModelNotRegisteredError("Settlement");
2947
+ }
2948
+ const {
2949
+ limit = 100,
2950
+ organizationId,
2951
+ payoutMethod,
2952
+ dryRun = false
2953
+ } = options;
2954
+ const SettlementModel = this.models.Settlement;
2955
+ const query = {
2956
+ status: SETTLEMENT_STATUS.PENDING,
2957
+ scheduledAt: { $lte: /* @__PURE__ */ new Date() }
2958
+ };
2959
+ if (organizationId) query.organizationId = organizationId;
2960
+ if (payoutMethod) query.payoutMethod = payoutMethod;
2961
+ const settlements = await SettlementModel.find(query).limit(limit).sort({ scheduledAt: 1 });
2962
+ const result = {
2963
+ processed: 0,
2964
+ succeeded: 0,
2965
+ failed: 0,
2966
+ settlements: [],
2967
+ errors: []
2968
+ };
2969
+ if (dryRun) {
2970
+ this.logger.info("Dry run: would process settlements", { count: settlements.length });
2971
+ result.settlements = settlements;
2972
+ return result;
2973
+ }
2974
+ for (const settlement of settlements) {
2975
+ result.processed++;
2976
+ try {
2977
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
2978
+ settlement.status,
2979
+ SETTLEMENT_STATUS.PROCESSING,
2980
+ settlement._id.toString(),
2981
+ {
2982
+ changedBy: "system",
2983
+ reason: "Settlement processing started",
2984
+ metadata: { recipientId: settlement.recipientId, amount: settlement.amount }
2985
+ }
2986
+ );
2987
+ settlement.status = SETTLEMENT_STATUS.PROCESSING;
2988
+ settlement.processedAt = /* @__PURE__ */ new Date();
2989
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
2990
+ await settlement.save();
2991
+ result.succeeded++;
2992
+ result.settlements.push(settlement);
2993
+ this.events.emit("settlement.processing", {
2994
+ settlement,
2995
+ processedAt: settlement.processedAt
2996
+ });
2997
+ } catch (error) {
2998
+ result.failed++;
2999
+ result.errors.push({
3000
+ settlementId: settlement._id.toString(),
3001
+ error: error.message
3002
+ });
3003
+ this.logger.error("Failed to process settlement", {
3004
+ settlementId: settlement._id,
3005
+ error
3006
+ });
3007
+ }
3008
+ }
3009
+ this.logger.info("Processed settlements", result);
3010
+ return result;
3011
+ }
3012
+ /**
3013
+ * Mark settlement as completed
3014
+ * Call this after bank confirms the transfer
3015
+ *
3016
+ * @param settlementId - Settlement ID
3017
+ * @param details - Completion details
3018
+ * @returns Updated settlement
3019
+ */
3020
+ async complete(settlementId, details = {}) {
3021
+ if (!this.models.Settlement) {
3022
+ throw new ModelNotRegisteredError("Settlement");
3023
+ }
3024
+ const SettlementModel = this.models.Settlement;
3025
+ const settlement = await SettlementModel.findById(settlementId);
3026
+ if (!settlement) {
3027
+ throw new ValidationError("Settlement not found", { settlementId });
3028
+ }
3029
+ if (settlement.status !== SETTLEMENT_STATUS.PROCESSING && settlement.status !== SETTLEMENT_STATUS.PENDING) {
3030
+ throw new InvalidStateTransitionError(
3031
+ "complete",
3032
+ SETTLEMENT_STATUS.PROCESSING,
3033
+ settlement.status,
3034
+ "Only processing or pending settlements can be completed"
3035
+ );
3036
+ }
3037
+ const {
3038
+ transferReference,
3039
+ transferredAt = /* @__PURE__ */ new Date(),
3040
+ transactionHash,
3041
+ notes,
3042
+ metadata = {}
3043
+ } = details;
3044
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3045
+ settlement.status,
3046
+ SETTLEMENT_STATUS.COMPLETED,
3047
+ settlement._id.toString(),
3048
+ {
3049
+ changedBy: "system",
3050
+ reason: "Settlement completed successfully",
3051
+ metadata: {
3052
+ transferReference,
3053
+ transferredAt,
3054
+ transactionHash,
3055
+ payoutMethod: settlement.payoutMethod,
3056
+ amount: settlement.amount
3057
+ }
3058
+ }
3059
+ );
3060
+ settlement.status = SETTLEMENT_STATUS.COMPLETED;
3061
+ settlement.completedAt = /* @__PURE__ */ new Date();
3062
+ if (settlement.payoutMethod === "bank_transfer" && transferReference) {
3063
+ settlement.bankTransferDetails = {
3064
+ ...settlement.bankTransferDetails,
3065
+ transferReference,
3066
+ transferredAt
3067
+ };
3068
+ } else if (settlement.payoutMethod === "crypto" && transactionHash) {
3069
+ settlement.cryptoDetails = {
3070
+ ...settlement.cryptoDetails,
3071
+ transactionHash,
3072
+ transferredAt
3073
+ };
3074
+ } else if (settlement.payoutMethod === "mobile_wallet") {
3075
+ settlement.mobileWalletDetails = {
3076
+ ...settlement.mobileWalletDetails,
3077
+ transferredAt
3078
+ };
3079
+ } else if (settlement.payoutMethod === "platform_balance") {
3080
+ settlement.platformBalanceDetails = {
3081
+ ...settlement.platformBalanceDetails,
3082
+ appliedAt: transferredAt
3083
+ };
3084
+ }
3085
+ if (notes) settlement.notes = notes;
3086
+ settlement.metadata = { ...settlement.metadata, ...metadata };
3087
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3088
+ await settlement.save();
3089
+ this.events.emit("settlement.completed", {
3090
+ settlement,
3091
+ completedAt: settlement.completedAt
3092
+ });
3093
+ this.logger.info("Settlement completed", {
3094
+ settlementId: settlement._id,
3095
+ recipientId: settlement.recipientId,
3096
+ amount: settlement.amount
3097
+ });
3098
+ return settlement;
3099
+ }
3100
+ /**
3101
+ * Mark settlement as failed
3102
+ *
3103
+ * @param settlementId - Settlement ID
3104
+ * @param reason - Failure reason
3105
+ * @returns Updated settlement
3106
+ */
3107
+ async fail(settlementId, reason, options = {}) {
3108
+ if (!this.models.Settlement) {
3109
+ throw new ModelNotRegisteredError("Settlement");
3110
+ }
3111
+ const SettlementModel = this.models.Settlement;
3112
+ const settlement = await SettlementModel.findById(settlementId);
3113
+ if (!settlement) {
3114
+ throw new ValidationError("Settlement not found", { settlementId });
3115
+ }
3116
+ const { code, retry: retry2 = false } = options;
3117
+ if (retry2) {
3118
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3119
+ settlement.status,
3120
+ SETTLEMENT_STATUS.PENDING,
3121
+ settlement._id.toString(),
3122
+ {
3123
+ changedBy: "system",
3124
+ reason: `Settlement failed, retrying: ${reason}`,
3125
+ metadata: {
3126
+ failureReason: reason,
3127
+ failureCode: code,
3128
+ retryCount: (settlement.retryCount || 0) + 1,
3129
+ scheduledAt: new Date(Date.now() + 60 * 60 * 1e3)
3130
+ }
3131
+ }
3132
+ );
3133
+ settlement.status = SETTLEMENT_STATUS.PENDING;
3134
+ settlement.retryCount = (settlement.retryCount || 0) + 1;
3135
+ settlement.scheduledAt = new Date(Date.now() + 60 * 60 * 1e3);
3136
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3137
+ } else {
3138
+ const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
3139
+ settlement.status,
3140
+ SETTLEMENT_STATUS.FAILED,
3141
+ settlement._id.toString(),
3142
+ {
3143
+ changedBy: "system",
3144
+ reason: `Settlement failed: ${reason}`,
3145
+ metadata: {
3146
+ failureReason: reason,
3147
+ failureCode: code
3148
+ }
3149
+ }
3150
+ );
3151
+ settlement.status = SETTLEMENT_STATUS.FAILED;
3152
+ settlement.failedAt = /* @__PURE__ */ new Date();
3153
+ Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
3154
+ }
3155
+ settlement.failureReason = reason;
3156
+ if (code) settlement.failureCode = code;
3157
+ await settlement.save();
3158
+ this.events.emit("settlement.failed", {
3159
+ settlement,
3160
+ reason,
3161
+ code,
3162
+ retry: retry2
3163
+ });
3164
+ this.logger.warn("Settlement failed", {
3165
+ settlementId: settlement._id,
3166
+ reason,
3167
+ retry: retry2
3168
+ });
3169
+ return settlement;
3170
+ }
3171
+ /**
3172
+ * List settlements with filters
3173
+ *
3174
+ * @param filters - Query filters
3175
+ * @returns Settlements
3176
+ */
3177
+ async list(filters = {}) {
3178
+ if (!this.models.Settlement) {
3179
+ throw new ModelNotRegisteredError("Settlement");
3180
+ }
3181
+ const SettlementModel = this.models.Settlement;
3182
+ const {
3183
+ organizationId,
3184
+ recipientId,
3185
+ status,
3186
+ type,
3187
+ payoutMethod,
3188
+ scheduledAfter,
3189
+ scheduledBefore,
3190
+ limit = 50,
3191
+ skip = 0,
3192
+ sort = { createdAt: -1 }
3193
+ } = filters;
3194
+ const query = {};
3195
+ if (organizationId) query.organizationId = organizationId;
3196
+ if (recipientId) query.recipientId = recipientId;
3197
+ if (status) query.status = Array.isArray(status) ? { $in: status } : status;
3198
+ if (type) query.type = type;
3199
+ if (payoutMethod) query.payoutMethod = payoutMethod;
3200
+ if (scheduledAfter || scheduledBefore) {
3201
+ query.scheduledAt = {};
3202
+ if (scheduledAfter) query.scheduledAt.$gte = scheduledAfter;
3203
+ if (scheduledBefore) query.scheduledAt.$lte = scheduledBefore;
3204
+ }
3205
+ const settlements = await SettlementModel.find(query).limit(limit).skip(skip).sort(sort);
3206
+ return settlements;
3207
+ }
3208
+ /**
3209
+ * Get payout summary for recipient
3210
+ *
3211
+ * @param recipientId - Recipient ID
3212
+ * @param options - Summary options
3213
+ * @returns Settlement summary
3214
+ */
3215
+ async getSummary(recipientId, options = {}) {
3216
+ if (!this.models.Settlement) {
3217
+ throw new ModelNotRegisteredError("Settlement");
3218
+ }
3219
+ const { organizationId, startDate, endDate } = options;
3220
+ const SettlementModel = this.models.Settlement;
3221
+ const query = { recipientId };
3222
+ if (organizationId) query.organizationId = organizationId;
3223
+ if (startDate || endDate) {
3224
+ query.createdAt = {};
3225
+ if (startDate) query.createdAt.$gte = startDate;
3226
+ if (endDate) query.createdAt.$lte = endDate;
3227
+ }
3228
+ const settlements = await SettlementModel.find(query);
3229
+ const summary = {
3230
+ recipientId,
3231
+ totalPending: 0,
3232
+ totalProcessing: 0,
3233
+ totalCompleted: 0,
3234
+ totalFailed: 0,
3235
+ amountPending: 0,
3236
+ amountCompleted: 0,
3237
+ amountFailed: 0,
3238
+ currency: settlements[0]?.currency || "USD",
3239
+ settlements: {
3240
+ pending: 0,
3241
+ processing: 0,
3242
+ completed: 0,
3243
+ failed: 0,
3244
+ cancelled: 0
3245
+ }
3246
+ };
3247
+ for (const settlement of settlements) {
3248
+ summary.settlements[settlement.status]++;
3249
+ if (settlement.status === SETTLEMENT_STATUS.PENDING) {
3250
+ summary.totalPending++;
3251
+ summary.amountPending += settlement.amount;
3252
+ } else if (settlement.status === SETTLEMENT_STATUS.PROCESSING) {
3253
+ summary.totalProcessing++;
3254
+ } else if (settlement.status === SETTLEMENT_STATUS.COMPLETED) {
3255
+ summary.totalCompleted++;
3256
+ summary.amountCompleted += settlement.amount;
3257
+ if (!summary.lastSettlementDate || settlement.completedAt > summary.lastSettlementDate) {
3258
+ summary.lastSettlementDate = settlement.completedAt;
3259
+ }
3260
+ } else if (settlement.status === SETTLEMENT_STATUS.FAILED) {
3261
+ summary.totalFailed++;
3262
+ summary.amountFailed += settlement.amount;
3263
+ }
3264
+ }
3265
+ return summary;
3266
+ }
3267
+ /**
3268
+ * Get settlement by ID
3269
+ *
3270
+ * @param settlementId - Settlement ID
3271
+ * @returns Settlement
3272
+ */
3273
+ async get(settlementId) {
3274
+ if (!this.models.Settlement) {
3275
+ throw new ModelNotRegisteredError("Settlement");
3276
+ }
3277
+ const SettlementModel = this.models.Settlement;
3278
+ const settlement = await SettlementModel.findById(settlementId);
3279
+ if (!settlement) {
3280
+ throw new ValidationError("Settlement not found", { settlementId });
3281
+ }
3282
+ return settlement;
3283
+ }
3284
+ };
3285
+
3286
+ export { EscrowService, MonetizationService, PaymentService, SettlementService, TransactionService };
3287
+ //# sourceMappingURL=index.js.map
3288
+ //# sourceMappingURL=index.js.map