@classytic/revenue 1.0.6 → 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 (54) hide show
  1. package/README.md +581 -633
  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 +2372 -786
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/enums/index.d.ts +29 -8
  12. package/dist/enums/index.js +41 -8
  13. package/dist/enums/index.js.map +1 -1
  14. package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
  15. package/dist/{index-BnEXsnLJ.d.ts → index-DxIK0UmZ.d.ts} +281 -26
  16. package/dist/index-EnfKzDbs.d.ts +806 -0
  17. package/dist/{index-C5SsOrV0.d.ts → index-cLJBLUvx.d.ts} +55 -111
  18. package/dist/index.d.ts +16 -16
  19. package/dist/index.js +2558 -2192
  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 +1653 -49
  35. package/dist/schemas/index.js +233 -19
  36. package/dist/schemas/index.js.map +1 -1
  37. package/dist/schemas/validation.d.ts +4 -4
  38. package/dist/schemas/validation.js +16 -15
  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 +351 -289
  46. package/dist/utils/index.js.map +1 -1
  47. package/package.json +22 -9
  48. package/dist/actions-Ctf2XUL-.d.ts +0 -519
  49. package/dist/payment.enums-B_RwB8iR.d.ts +0 -184
  50. package/dist/services/index.d.ts +0 -3
  51. package/dist/services/index.js +0 -1702
  52. package/dist/services/index.js.map +0 -1
  53. package/dist/split.schema-DLVF3XBI.d.ts +0 -1122
  54. package/dist/transaction.enums-7uBnuswI.d.ts +0 -87
@@ -1,1702 +0,0 @@
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/utils/hooks.ts
181
- function triggerHook(hooks, event, data, logger) {
182
- const handlers = hooks[event] ?? [];
183
- if (handlers.length === 0) {
184
- return;
185
- }
186
- Promise.all(
187
- handlers.map(
188
- (handler) => Promise.resolve(handler(data)).catch((error) => {
189
- logger.error(`Hook "${event}" failed:`, {
190
- error: error.message,
191
- stack: error.stack,
192
- event,
193
- // Don't log full data (could be huge)
194
- dataKeys: Object.keys(data)
195
- });
196
- })
197
- )
198
- ).catch(() => {
199
- });
200
- }
201
-
202
- // src/enums/transaction.enums.ts
203
- var TRANSACTION_TYPE = {
204
- INCOME: "income",
205
- EXPENSE: "expense"
206
- };
207
- var TRANSACTION_TYPE_VALUES = Object.values(
208
- TRANSACTION_TYPE
209
- );
210
- var TRANSACTION_STATUS = {
211
- PENDING: "pending",
212
- PAYMENT_INITIATED: "payment_initiated",
213
- PROCESSING: "processing",
214
- REQUIRES_ACTION: "requires_action",
215
- VERIFIED: "verified",
216
- COMPLETED: "completed",
217
- FAILED: "failed",
218
- CANCELLED: "cancelled",
219
- EXPIRED: "expired",
220
- REFUNDED: "refunded",
221
- PARTIALLY_REFUNDED: "partially_refunded"
222
- };
223
- var TRANSACTION_STATUS_VALUES = Object.values(
224
- TRANSACTION_STATUS
225
- );
226
- var LIBRARY_CATEGORIES = {
227
- SUBSCRIPTION: "subscription",
228
- PURCHASE: "purchase"
229
- };
230
- var LIBRARY_CATEGORY_VALUES = Object.values(
231
- LIBRARY_CATEGORIES
232
- );
233
- new Set(TRANSACTION_TYPE_VALUES);
234
- new Set(
235
- TRANSACTION_STATUS_VALUES
236
- );
237
- new Set(LIBRARY_CATEGORY_VALUES);
238
-
239
- // src/utils/category-resolver.ts
240
- function resolveCategory(entity, monetizationType, categoryMappings = {}) {
241
- if (entity && categoryMappings[entity]) {
242
- return categoryMappings[entity];
243
- }
244
- switch (monetizationType) {
245
- case "subscription":
246
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
247
- // 'subscription'
248
- case "purchase":
249
- return LIBRARY_CATEGORIES.PURCHASE;
250
- // 'purchase'
251
- default:
252
- return LIBRARY_CATEGORIES.SUBSCRIPTION;
253
- }
254
- }
255
-
256
- // src/utils/commission.ts
257
- function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
258
- if (!commissionRate || commissionRate <= 0) {
259
- return null;
260
- }
261
- if (amount < 0) {
262
- throw new Error("Transaction amount cannot be negative");
263
- }
264
- if (commissionRate < 0 || commissionRate > 1) {
265
- throw new Error("Commission rate must be between 0 and 1");
266
- }
267
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
268
- throw new Error("Gateway fee rate must be between 0 and 1");
269
- }
270
- const grossAmount = Math.round(amount * commissionRate * 100) / 100;
271
- const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
272
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
273
- return {
274
- rate: commissionRate,
275
- grossAmount,
276
- gatewayFeeRate,
277
- gatewayFeeAmount,
278
- netAmount,
279
- status: "pending"
280
- };
281
- }
282
- function reverseCommission(originalCommission, originalAmount, refundAmount) {
283
- if (!originalCommission?.netAmount) {
284
- return null;
285
- }
286
- const refundRatio = refundAmount / originalAmount;
287
- const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
288
- const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
289
- const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
290
- return {
291
- rate: originalCommission.rate,
292
- grossAmount: reversedGrossAmount,
293
- gatewayFeeRate: originalCommission.gatewayFeeRate,
294
- gatewayFeeAmount: reversedGatewayFee,
295
- netAmount: reversedNetAmount,
296
- status: "waived"
297
- // Commission waived due to refund
298
- };
299
- }
300
-
301
- // src/enums/monetization.enums.ts
302
- var MONETIZATION_TYPES = {
303
- FREE: "free",
304
- PURCHASE: "purchase",
305
- SUBSCRIPTION: "subscription"
306
- };
307
- var MONETIZATION_TYPE_VALUES = Object.values(
308
- MONETIZATION_TYPES
309
- );
310
- new Set(MONETIZATION_TYPE_VALUES);
311
-
312
- // src/services/monetization.service.ts
313
- var MonetizationService = class {
314
- models;
315
- providers;
316
- config;
317
- hooks;
318
- logger;
319
- constructor(container) {
320
- this.models = container.get("models");
321
- this.providers = container.get("providers");
322
- this.config = container.get("config");
323
- this.hooks = container.get("hooks");
324
- this.logger = container.get("logger");
325
- }
326
- /**
327
- * Create a new monetization (purchase, subscription, or free item)
328
- *
329
- * @param params - Monetization parameters
330
- *
331
- * @example
332
- * // One-time purchase
333
- * await revenue.monetization.create({
334
- * data: {
335
- * organizationId: '...',
336
- * customerId: '...',
337
- * referenceId: order._id,
338
- * referenceModel: 'Order',
339
- * },
340
- * planKey: 'one_time',
341
- * monetizationType: 'purchase',
342
- * gateway: 'bkash',
343
- * amount: 1500,
344
- * });
345
- *
346
- * // Recurring subscription
347
- * await revenue.monetization.create({
348
- * data: {
349
- * organizationId: '...',
350
- * customerId: '...',
351
- * referenceId: subscription._id,
352
- * referenceModel: 'Subscription',
353
- * },
354
- * planKey: 'monthly',
355
- * monetizationType: 'subscription',
356
- * gateway: 'stripe',
357
- * amount: 2000,
358
- * });
359
- *
360
- * @returns Result with subscription, transaction, and paymentIntent
361
- */
362
- async create(params) {
363
- const {
364
- data,
365
- planKey,
366
- amount,
367
- currency = "BDT",
368
- gateway = "manual",
369
- entity = null,
370
- monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
371
- paymentData,
372
- metadata = {},
373
- idempotencyKey = null
374
- } = params;
375
- if (!planKey) {
376
- throw new MissingRequiredFieldError("planKey");
377
- }
378
- if (amount < 0) {
379
- throw new InvalidAmountError(amount);
380
- }
381
- const isFree = amount === 0;
382
- const provider = this.providers[gateway];
383
- if (!provider) {
384
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
385
- }
386
- let paymentIntent = null;
387
- let transaction = null;
388
- if (!isFree) {
389
- try {
390
- paymentIntent = await provider.createIntent({
391
- amount,
392
- currency,
393
- metadata: {
394
- ...metadata,
395
- type: "subscription",
396
- planKey
397
- }
398
- });
399
- } catch (error) {
400
- throw new PaymentIntentCreationError(gateway, error);
401
- }
402
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
403
- const transactionType = this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_TYPE.INCOME;
404
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
405
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
406
- const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
407
- const TransactionModel = this.models.Transaction;
408
- transaction = await TransactionModel.create({
409
- organizationId: data.organizationId,
410
- customerId: data.customerId ?? null,
411
- amount,
412
- currency,
413
- category,
414
- type: transactionType,
415
- method: paymentData?.method ?? "manual",
416
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
417
- gateway: {
418
- type: gateway,
419
- sessionId: paymentIntent.sessionId,
420
- paymentIntentId: paymentIntent.paymentIntentId,
421
- provider: paymentIntent.provider,
422
- metadata: paymentIntent.metadata
423
- },
424
- paymentDetails: {
425
- provider: gateway,
426
- ...paymentData
427
- },
428
- ...commission && { commission },
429
- // Only include if commission exists
430
- // Polymorphic reference (top-level, not metadata)
431
- ...data.referenceId && { referenceId: data.referenceId },
432
- ...data.referenceModel && { referenceModel: data.referenceModel },
433
- metadata: {
434
- ...metadata,
435
- planKey,
436
- entity,
437
- monetizationType,
438
- paymentIntentId: paymentIntent.id
439
- },
440
- idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
441
- });
442
- }
443
- let subscription = null;
444
- if (this.models.Subscription) {
445
- const SubscriptionModel = this.models.Subscription;
446
- const subscriptionData = {
447
- organizationId: data.organizationId,
448
- customerId: data.customerId ?? null,
449
- planKey,
450
- amount,
451
- currency,
452
- status: isFree ? "active" : "pending",
453
- isActive: isFree,
454
- gateway,
455
- transactionId: transaction?._id ?? null,
456
- paymentIntentId: paymentIntent?.id ?? null,
457
- metadata: {
458
- ...metadata,
459
- isFree,
460
- entity,
461
- monetizationType
462
- },
463
- ...data
464
- };
465
- delete subscriptionData.referenceId;
466
- delete subscriptionData.referenceModel;
467
- subscription = await SubscriptionModel.create(subscriptionData);
468
- }
469
- const eventData = {
470
- subscription,
471
- transaction,
472
- paymentIntent,
473
- isFree,
474
- monetizationType
475
- };
476
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
477
- this._triggerHook("purchase.created", eventData);
478
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
479
- this._triggerHook("subscription.created", eventData);
480
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
481
- this._triggerHook("free.created", eventData);
482
- }
483
- this._triggerHook("monetization.created", eventData);
484
- return {
485
- subscription,
486
- transaction,
487
- paymentIntent
488
- };
489
- }
490
- /**
491
- * Activate subscription after payment verification
492
- *
493
- * @param subscriptionId - Subscription ID or transaction ID
494
- * @param options - Activation options
495
- * @returns Updated subscription
496
- */
497
- async activate(subscriptionId, options = {}) {
498
- const { timestamp = /* @__PURE__ */ new Date() } = options;
499
- if (!this.models.Subscription) {
500
- throw new ModelNotRegisteredError("Subscription");
501
- }
502
- const SubscriptionModel = this.models.Subscription;
503
- const subscription = await SubscriptionModel.findById(subscriptionId);
504
- if (!subscription) {
505
- throw new SubscriptionNotFoundError(subscriptionId);
506
- }
507
- if (subscription.isActive) {
508
- this.logger.warn("Subscription already active", { subscriptionId });
509
- return subscription;
510
- }
511
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
512
- subscription.isActive = true;
513
- subscription.status = "active";
514
- subscription.startDate = timestamp;
515
- subscription.endDate = periodEnd;
516
- subscription.activatedAt = timestamp;
517
- await subscription.save();
518
- this._triggerHook("subscription.activated", {
519
- subscription,
520
- activatedAt: timestamp
521
- });
522
- return subscription;
523
- }
524
- /**
525
- * Renew subscription
526
- *
527
- * @param subscriptionId - Subscription ID
528
- * @param params - Renewal parameters
529
- * @returns { subscription, transaction, paymentIntent }
530
- */
531
- async renew(subscriptionId, params = {}) {
532
- const {
533
- gateway = "manual",
534
- entity = null,
535
- paymentData,
536
- metadata = {},
537
- idempotencyKey = null
538
- } = params;
539
- if (!this.models.Subscription) {
540
- throw new ModelNotRegisteredError("Subscription");
541
- }
542
- const SubscriptionModel = this.models.Subscription;
543
- const subscription = await SubscriptionModel.findById(subscriptionId);
544
- if (!subscription) {
545
- throw new SubscriptionNotFoundError(subscriptionId);
546
- }
547
- if (subscription.amount === 0) {
548
- throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
549
- }
550
- const provider = this.providers[gateway];
551
- if (!provider) {
552
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
553
- }
554
- let paymentIntent = null;
555
- try {
556
- paymentIntent = await provider.createIntent({
557
- amount: subscription.amount,
558
- currency: subscription.currency ?? "BDT",
559
- metadata: {
560
- ...metadata,
561
- type: "subscription_renewal",
562
- subscriptionId: subscription._id.toString()
563
- }
564
- });
565
- } catch (error) {
566
- this.logger.error("Failed to create payment intent for renewal:", error);
567
- throw new PaymentIntentCreationError(gateway, error);
568
- }
569
- const effectiveEntity = entity ?? subscription.metadata?.entity;
570
- const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
571
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
572
- const transactionType = this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_TYPE.INCOME;
573
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
574
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
575
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
576
- const TransactionModel = this.models.Transaction;
577
- const transaction = await TransactionModel.create({
578
- organizationId: subscription.organizationId,
579
- customerId: subscription.customerId,
580
- amount: subscription.amount,
581
- currency: subscription.currency ?? "BDT",
582
- category,
583
- type: transactionType,
584
- method: paymentData?.method ?? "manual",
585
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
586
- gateway: {
587
- type: gateway,
588
- sessionId: paymentIntent.sessionId,
589
- paymentIntentId: paymentIntent.paymentIntentId,
590
- provider: paymentIntent.provider,
591
- metadata: paymentIntent.metadata
592
- },
593
- paymentDetails: {
594
- provider: gateway,
595
- ...paymentData
596
- },
597
- ...commission && { commission },
598
- // Only include if commission exists
599
- // Polymorphic reference to subscription
600
- referenceId: subscription._id,
601
- referenceModel: "Subscription",
602
- metadata: {
603
- ...metadata,
604
- subscriptionId: subscription._id.toString(),
605
- // Keep for backward compat
606
- entity: effectiveEntity,
607
- monetizationType: effectiveMonetizationType,
608
- isRenewal: true,
609
- paymentIntentId: paymentIntent.id
610
- },
611
- idempotencyKey: idempotencyKey ?? `renewal_${nanoid(16)}`
612
- });
613
- subscription.status = "pending_renewal";
614
- subscription.renewalTransactionId = transaction._id;
615
- subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
616
- await subscription.save();
617
- this._triggerHook("subscription.renewed", {
618
- subscription,
619
- transaction,
620
- paymentIntent,
621
- renewalCount: subscription.renewalCount
622
- });
623
- return {
624
- subscription,
625
- transaction,
626
- paymentIntent
627
- };
628
- }
629
- /**
630
- * Cancel subscription
631
- *
632
- * @param subscriptionId - Subscription ID
633
- * @param options - Cancellation options
634
- * @returns Updated subscription
635
- */
636
- async cancel(subscriptionId, options = {}) {
637
- const { immediate = false, reason = null } = options;
638
- if (!this.models.Subscription) {
639
- throw new ModelNotRegisteredError("Subscription");
640
- }
641
- const SubscriptionModel = this.models.Subscription;
642
- const subscription = await SubscriptionModel.findById(subscriptionId);
643
- if (!subscription) {
644
- throw new SubscriptionNotFoundError(subscriptionId);
645
- }
646
- const now = /* @__PURE__ */ new Date();
647
- if (immediate) {
648
- subscription.isActive = false;
649
- subscription.status = "cancelled";
650
- subscription.canceledAt = now;
651
- subscription.cancellationReason = reason;
652
- } else {
653
- subscription.cancelAt = subscription.endDate ?? now;
654
- subscription.cancellationReason = reason;
655
- }
656
- await subscription.save();
657
- this._triggerHook("subscription.cancelled", {
658
- subscription,
659
- immediate,
660
- reason,
661
- canceledAt: immediate ? now : subscription.cancelAt
662
- });
663
- return subscription;
664
- }
665
- /**
666
- * Pause subscription
667
- *
668
- * @param subscriptionId - Subscription ID
669
- * @param options - Pause options
670
- * @returns Updated subscription
671
- */
672
- async pause(subscriptionId, options = {}) {
673
- const { reason = null } = options;
674
- if (!this.models.Subscription) {
675
- throw new ModelNotRegisteredError("Subscription");
676
- }
677
- const SubscriptionModel = this.models.Subscription;
678
- const subscription = await SubscriptionModel.findById(subscriptionId);
679
- if (!subscription) {
680
- throw new SubscriptionNotFoundError(subscriptionId);
681
- }
682
- if (!subscription.isActive) {
683
- throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
684
- }
685
- const pausedAt = /* @__PURE__ */ new Date();
686
- subscription.isActive = false;
687
- subscription.status = "paused";
688
- subscription.pausedAt = pausedAt;
689
- subscription.pauseReason = reason;
690
- await subscription.save();
691
- this._triggerHook("subscription.paused", {
692
- subscription,
693
- reason,
694
- pausedAt
695
- });
696
- return subscription;
697
- }
698
- /**
699
- * Resume subscription
700
- *
701
- * @param subscriptionId - Subscription ID
702
- * @param options - Resume options
703
- * @returns Updated subscription
704
- */
705
- async resume(subscriptionId, options = {}) {
706
- const { extendPeriod = false } = options;
707
- if (!this.models.Subscription) {
708
- throw new ModelNotRegisteredError("Subscription");
709
- }
710
- const SubscriptionModel = this.models.Subscription;
711
- const subscription = await SubscriptionModel.findById(subscriptionId);
712
- if (!subscription) {
713
- throw new SubscriptionNotFoundError(subscriptionId);
714
- }
715
- if (!subscription.pausedAt) {
716
- throw new InvalidStateTransitionError(
717
- "resume",
718
- "paused",
719
- subscription.status,
720
- "Only paused subscriptions can be resumed"
721
- );
722
- }
723
- const now = /* @__PURE__ */ new Date();
724
- const pausedAt = new Date(subscription.pausedAt);
725
- const pauseDuration = now.getTime() - pausedAt.getTime();
726
- subscription.isActive = true;
727
- subscription.status = "active";
728
- subscription.pausedAt = null;
729
- subscription.pauseReason = null;
730
- if (extendPeriod && subscription.endDate) {
731
- const currentEnd = new Date(subscription.endDate);
732
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
733
- }
734
- await subscription.save();
735
- this._triggerHook("subscription.resumed", {
736
- subscription,
737
- extendPeriod,
738
- pauseDuration,
739
- resumedAt: now
740
- });
741
- return subscription;
742
- }
743
- /**
744
- * List subscriptions with filters
745
- *
746
- * @param filters - Query filters
747
- * @param options - Query options (limit, skip, sort)
748
- * @returns Subscriptions
749
- */
750
- async list(filters = {}, options = {}) {
751
- if (!this.models.Subscription) {
752
- throw new ModelNotRegisteredError("Subscription");
753
- }
754
- const SubscriptionModel = this.models.Subscription;
755
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
756
- const subscriptions = await SubscriptionModel.find(filters).limit(limit).skip(skip).sort(sort);
757
- return subscriptions;
758
- }
759
- /**
760
- * Get subscription by ID
761
- *
762
- * @param subscriptionId - Subscription ID
763
- * @returns Subscription
764
- */
765
- async get(subscriptionId) {
766
- if (!this.models.Subscription) {
767
- throw new ModelNotRegisteredError("Subscription");
768
- }
769
- const SubscriptionModel = this.models.Subscription;
770
- const subscription = await SubscriptionModel.findById(subscriptionId);
771
- if (!subscription) {
772
- throw new SubscriptionNotFoundError(subscriptionId);
773
- }
774
- return subscription;
775
- }
776
- /**
777
- * Calculate period end date based on plan key
778
- * @private
779
- */
780
- _calculatePeriodEnd(planKey, startDate = /* @__PURE__ */ new Date()) {
781
- const start = new Date(startDate);
782
- const end = new Date(start);
783
- switch (planKey) {
784
- case "monthly":
785
- end.setMonth(end.getMonth() + 1);
786
- break;
787
- case "quarterly":
788
- end.setMonth(end.getMonth() + 3);
789
- break;
790
- case "yearly":
791
- end.setFullYear(end.getFullYear() + 1);
792
- break;
793
- default:
794
- end.setDate(end.getDate() + 30);
795
- }
796
- return end;
797
- }
798
- /**
799
- * Trigger event hook (fire-and-forget, non-blocking)
800
- * @private
801
- */
802
- _triggerHook(event, data) {
803
- triggerHook(this.hooks, event, data, this.logger);
804
- }
805
- };
806
-
807
- // src/services/payment.service.ts
808
- var PaymentService = class {
809
- models;
810
- providers;
811
- config;
812
- hooks;
813
- logger;
814
- constructor(container) {
815
- this.models = container.get("models");
816
- this.providers = container.get("providers");
817
- this.config = container.get("config");
818
- this.hooks = container.get("hooks");
819
- this.logger = container.get("logger");
820
- }
821
- /**
822
- * Verify a payment
823
- *
824
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
825
- * @param options - Verification options
826
- * @returns { transaction, status }
827
- */
828
- async verify(paymentIntentId, options = {}) {
829
- const { verifiedBy = null } = options;
830
- const TransactionModel = this.models.Transaction;
831
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
832
- if (!transaction) {
833
- throw new TransactionNotFoundError(paymentIntentId);
834
- }
835
- if (transaction.status === "verified" || transaction.status === "completed") {
836
- throw new AlreadyVerifiedError(transaction._id.toString());
837
- }
838
- const gatewayType = transaction.gateway?.type ?? "manual";
839
- const provider = this.providers[gatewayType];
840
- if (!provider) {
841
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
842
- }
843
- let paymentResult = null;
844
- try {
845
- paymentResult = await provider.verifyPayment(paymentIntentId);
846
- } catch (error) {
847
- this.logger.error("Payment verification failed:", error);
848
- transaction.status = "failed";
849
- transaction.failureReason = error.message;
850
- transaction.metadata = {
851
- ...transaction.metadata,
852
- verificationError: error.message,
853
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
854
- };
855
- await transaction.save();
856
- this._triggerHook("payment.failed", {
857
- transaction,
858
- error: error.message,
859
- provider: gatewayType,
860
- paymentIntentId
861
- });
862
- throw new PaymentVerificationError(paymentIntentId, error.message);
863
- }
864
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
865
- throw new ValidationError(
866
- `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
867
- { expected: transaction.amount, actual: paymentResult.amount }
868
- );
869
- }
870
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
871
- throw new ValidationError(
872
- `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
873
- { expected: transaction.currency, actual: paymentResult.currency }
874
- );
875
- }
876
- transaction.status = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
877
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
878
- transaction.verifiedBy = verifiedBy;
879
- transaction.gateway = {
880
- ...transaction.gateway,
881
- type: transaction.gateway?.type ?? "manual",
882
- verificationData: paymentResult.metadata
883
- };
884
- await transaction.save();
885
- this._triggerHook("payment.verified", {
886
- transaction,
887
- paymentResult,
888
- verifiedBy
889
- });
890
- return {
891
- transaction,
892
- paymentResult,
893
- status: transaction.status
894
- };
895
- }
896
- /**
897
- * Get payment status
898
- *
899
- * @param paymentIntentId - Payment intent ID, session ID, or transaction ID
900
- * @returns { transaction, status }
901
- */
902
- async getStatus(paymentIntentId) {
903
- const TransactionModel = this.models.Transaction;
904
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
905
- if (!transaction) {
906
- throw new TransactionNotFoundError(paymentIntentId);
907
- }
908
- const gatewayType = transaction.gateway?.type ?? "manual";
909
- const provider = this.providers[gatewayType];
910
- if (!provider) {
911
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
912
- }
913
- let paymentResult = null;
914
- try {
915
- paymentResult = await provider.getStatus(paymentIntentId);
916
- } catch (error) {
917
- this.logger.warn("Failed to get payment status from provider:", error);
918
- return {
919
- transaction,
920
- status: transaction.status,
921
- provider: gatewayType
922
- };
923
- }
924
- return {
925
- transaction,
926
- paymentResult,
927
- status: paymentResult.status,
928
- provider: gatewayType
929
- };
930
- }
931
- /**
932
- * Refund a payment
933
- *
934
- * @param paymentId - Payment intent ID, session ID, or transaction ID
935
- * @param amount - Amount to refund (optional, full refund if not provided)
936
- * @param options - Refund options
937
- * @returns { transaction, refundResult }
938
- */
939
- async refund(paymentId, amount = null, options = {}) {
940
- const { reason = null } = options;
941
- const TransactionModel = this.models.Transaction;
942
- const transaction = await this._findTransaction(TransactionModel, paymentId);
943
- if (!transaction) {
944
- throw new TransactionNotFoundError(paymentId);
945
- }
946
- if (transaction.status !== "verified" && transaction.status !== "completed") {
947
- throw new RefundError(transaction._id.toString(), "Only verified/completed transactions can be refunded");
948
- }
949
- const gatewayType = transaction.gateway?.type ?? "manual";
950
- const provider = this.providers[gatewayType];
951
- if (!provider) {
952
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
953
- }
954
- const capabilities = provider.getCapabilities();
955
- if (!capabilities.supportsRefunds) {
956
- throw new RefundNotSupportedError(gatewayType);
957
- }
958
- const refundedSoFar = transaction.refundedAmount ?? 0;
959
- const refundableAmount = transaction.amount - refundedSoFar;
960
- const refundAmount = amount ?? refundableAmount;
961
- if (refundAmount <= 0) {
962
- throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
963
- }
964
- if (refundAmount > refundableAmount) {
965
- throw new ValidationError(
966
- `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
967
- { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
968
- );
969
- }
970
- let refundResult;
971
- try {
972
- refundResult = await provider.refund(paymentId, refundAmount, { reason: reason ?? void 0 });
973
- } catch (error) {
974
- this.logger.error("Refund failed:", error);
975
- throw new RefundError(paymentId, error.message);
976
- }
977
- const refundTransactionType = this.config.transactionTypeMapping?.refund ?? TRANSACTION_TYPE.EXPENSE;
978
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
979
- const refundTransaction = await TransactionModel.create({
980
- organizationId: transaction.organizationId,
981
- customerId: transaction.customerId,
982
- amount: refundAmount,
983
- currency: transaction.currency,
984
- category: transaction.category,
985
- type: refundTransactionType,
986
- // EXPENSE - money going out
987
- method: transaction.method ?? "manual",
988
- status: "completed",
989
- gateway: {
990
- type: transaction.gateway?.type ?? "manual",
991
- paymentIntentId: refundResult.id,
992
- provider: refundResult.provider
993
- },
994
- paymentDetails: transaction.paymentDetails,
995
- ...refundCommission && { commission: refundCommission },
996
- // Reversed commission
997
- // Polymorphic reference (copy from original transaction)
998
- ...transaction.referenceId && { referenceId: transaction.referenceId },
999
- ...transaction.referenceModel && { referenceModel: transaction.referenceModel },
1000
- metadata: {
1001
- ...transaction.metadata,
1002
- isRefund: true,
1003
- originalTransactionId: transaction._id.toString(),
1004
- refundReason: reason,
1005
- refundResult: refundResult.metadata
1006
- },
1007
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1008
- });
1009
- const isPartialRefund = refundAmount < transaction.amount;
1010
- transaction.status = isPartialRefund ? "partially_refunded" : "refunded";
1011
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1012
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1013
- transaction.metadata = {
1014
- ...transaction.metadata,
1015
- refundTransactionId: refundTransaction._id.toString(),
1016
- refundReason: reason
1017
- };
1018
- await transaction.save();
1019
- this._triggerHook("payment.refunded", {
1020
- transaction,
1021
- refundTransaction,
1022
- refundResult,
1023
- refundAmount,
1024
- reason,
1025
- isPartialRefund
1026
- });
1027
- return {
1028
- transaction,
1029
- refundTransaction,
1030
- refundResult,
1031
- status: transaction.status
1032
- };
1033
- }
1034
- /**
1035
- * Handle webhook from payment provider
1036
- *
1037
- * @param provider - Provider name
1038
- * @param payload - Webhook payload
1039
- * @param headers - Request headers
1040
- * @returns { event, transaction }
1041
- */
1042
- async handleWebhook(providerName, payload, headers = {}) {
1043
- const provider = this.providers[providerName];
1044
- if (!provider) {
1045
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1046
- }
1047
- const capabilities = provider.getCapabilities();
1048
- if (!capabilities.supportsWebhooks) {
1049
- throw new ProviderCapabilityError(providerName, "webhooks");
1050
- }
1051
- let webhookEvent;
1052
- try {
1053
- webhookEvent = await provider.handleWebhook(payload, headers);
1054
- } catch (error) {
1055
- this.logger.error("Webhook processing failed:", error);
1056
- throw new ProviderError(
1057
- `Webhook processing failed for ${providerName}: ${error.message}`,
1058
- "WEBHOOK_PROCESSING_FAILED",
1059
- { retryable: false }
1060
- );
1061
- }
1062
- if (!webhookEvent?.data?.sessionId && !webhookEvent?.data?.paymentIntentId) {
1063
- throw new ValidationError(
1064
- `Invalid webhook event structure from ${providerName}: missing sessionId or paymentIntentId`,
1065
- { provider: providerName, eventType: webhookEvent?.type }
1066
- );
1067
- }
1068
- const TransactionModel = this.models.Transaction;
1069
- let transaction = null;
1070
- if (webhookEvent.data.sessionId) {
1071
- transaction = await TransactionModel.findOne({
1072
- "gateway.sessionId": webhookEvent.data.sessionId
1073
- });
1074
- }
1075
- if (!transaction && webhookEvent.data.paymentIntentId) {
1076
- transaction = await TransactionModel.findOne({
1077
- "gateway.paymentIntentId": webhookEvent.data.paymentIntentId
1078
- });
1079
- }
1080
- if (!transaction) {
1081
- this.logger.warn("Transaction not found for webhook event", {
1082
- provider: providerName,
1083
- eventId: webhookEvent.id,
1084
- sessionId: webhookEvent.data.sessionId,
1085
- paymentIntentId: webhookEvent.data.paymentIntentId
1086
- });
1087
- throw new TransactionNotFoundError(
1088
- webhookEvent.data.sessionId ?? webhookEvent.data.paymentIntentId ?? "unknown"
1089
- );
1090
- }
1091
- if (webhookEvent.data.sessionId && !transaction.gateway?.sessionId) {
1092
- transaction.gateway = {
1093
- ...transaction.gateway,
1094
- type: transaction.gateway?.type ?? "manual",
1095
- sessionId: webhookEvent.data.sessionId
1096
- };
1097
- }
1098
- if (webhookEvent.data.paymentIntentId && !transaction.gateway?.paymentIntentId) {
1099
- transaction.gateway = {
1100
- ...transaction.gateway,
1101
- type: transaction.gateway?.type ?? "manual",
1102
- paymentIntentId: webhookEvent.data.paymentIntentId
1103
- };
1104
- }
1105
- if (transaction.webhook?.eventId === webhookEvent.id && transaction.webhook?.processedAt) {
1106
- this.logger.warn("Webhook already processed", {
1107
- transactionId: transaction._id,
1108
- eventId: webhookEvent.id
1109
- });
1110
- return {
1111
- event: webhookEvent,
1112
- transaction,
1113
- status: "already_processed"
1114
- };
1115
- }
1116
- transaction.webhook = {
1117
- eventId: webhookEvent.id,
1118
- eventType: webhookEvent.type,
1119
- receivedAt: /* @__PURE__ */ new Date(),
1120
- processedAt: /* @__PURE__ */ new Date(),
1121
- data: webhookEvent.data
1122
- };
1123
- if (webhookEvent.type === "payment.succeeded") {
1124
- transaction.status = "verified";
1125
- transaction.verifiedAt = webhookEvent.createdAt;
1126
- } else if (webhookEvent.type === "payment.failed") {
1127
- transaction.status = "failed";
1128
- } else if (webhookEvent.type === "refund.succeeded") {
1129
- transaction.status = "refunded";
1130
- transaction.refundedAt = webhookEvent.createdAt;
1131
- }
1132
- await transaction.save();
1133
- this._triggerHook(`payment.webhook.${webhookEvent.type}`, {
1134
- event: webhookEvent,
1135
- transaction
1136
- });
1137
- return {
1138
- event: webhookEvent,
1139
- transaction,
1140
- status: "processed"
1141
- };
1142
- }
1143
- /**
1144
- * List payments/transactions with filters
1145
- *
1146
- * @param filters - Query filters
1147
- * @param options - Query options (limit, skip, sort)
1148
- * @returns Transactions
1149
- */
1150
- async list(filters = {}, options = {}) {
1151
- const TransactionModel = this.models.Transaction;
1152
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
1153
- const transactions = await TransactionModel.find(filters).limit(limit).skip(skip).sort(sort);
1154
- return transactions;
1155
- }
1156
- /**
1157
- * Get payment/transaction by ID
1158
- *
1159
- * @param transactionId - Transaction ID
1160
- * @returns Transaction
1161
- */
1162
- async get(transactionId) {
1163
- const TransactionModel = this.models.Transaction;
1164
- const transaction = await TransactionModel.findById(transactionId);
1165
- if (!transaction) {
1166
- throw new TransactionNotFoundError(transactionId);
1167
- }
1168
- return transaction;
1169
- }
1170
- /**
1171
- * Get provider instance
1172
- *
1173
- * @param providerName - Provider name
1174
- * @returns Provider instance
1175
- */
1176
- getProvider(providerName) {
1177
- const provider = this.providers[providerName];
1178
- if (!provider) {
1179
- throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
1180
- }
1181
- return provider;
1182
- }
1183
- /**
1184
- * Trigger event hook (fire-and-forget, non-blocking)
1185
- * @private
1186
- */
1187
- _triggerHook(event, data) {
1188
- triggerHook(this.hooks, event, data, this.logger);
1189
- }
1190
- /**
1191
- * Find transaction by sessionId, paymentIntentId, or transaction ID
1192
- * @private
1193
- */
1194
- async _findTransaction(TransactionModel, identifier) {
1195
- let transaction = await TransactionModel.findOne({
1196
- "gateway.sessionId": identifier
1197
- });
1198
- if (!transaction) {
1199
- transaction = await TransactionModel.findOne({
1200
- "gateway.paymentIntentId": identifier
1201
- });
1202
- }
1203
- if (!transaction) {
1204
- transaction = await TransactionModel.findById(identifier);
1205
- }
1206
- return transaction;
1207
- }
1208
- };
1209
-
1210
- // src/services/transaction.service.ts
1211
- var TransactionService = class {
1212
- models;
1213
- hooks;
1214
- logger;
1215
- constructor(container) {
1216
- this.models = container.get("models");
1217
- this.hooks = container.get("hooks");
1218
- this.logger = container.get("logger");
1219
- }
1220
- /**
1221
- * Get transaction by ID
1222
- *
1223
- * @param transactionId - Transaction ID
1224
- * @returns Transaction
1225
- */
1226
- async get(transactionId) {
1227
- const TransactionModel = this.models.Transaction;
1228
- const transaction = await TransactionModel.findById(transactionId);
1229
- if (!transaction) {
1230
- throw new TransactionNotFoundError(transactionId);
1231
- }
1232
- return transaction;
1233
- }
1234
- /**
1235
- * List transactions with filters
1236
- *
1237
- * @param filters - Query filters
1238
- * @param options - Query options (limit, skip, sort, populate)
1239
- * @returns { transactions, total, page, limit }
1240
- */
1241
- async list(filters = {}, options = {}) {
1242
- const TransactionModel = this.models.Transaction;
1243
- const {
1244
- limit = 50,
1245
- skip = 0,
1246
- page = null,
1247
- sort = { createdAt: -1 },
1248
- populate = []
1249
- } = options;
1250
- const actualSkip = page ? (page - 1) * limit : skip;
1251
- let query = TransactionModel.find(filters).limit(limit).skip(actualSkip).sort(sort);
1252
- if (populate.length > 0 && typeof query.populate === "function") {
1253
- populate.forEach((field) => {
1254
- query = query.populate(field);
1255
- });
1256
- }
1257
- const transactions = await query;
1258
- const model = TransactionModel;
1259
- const total = await (model.countDocuments ? model.countDocuments(filters) : model.count?.(filters)) ?? 0;
1260
- return {
1261
- transactions,
1262
- total,
1263
- page: page ?? Math.floor(actualSkip / limit) + 1,
1264
- limit,
1265
- pages: Math.ceil(total / limit)
1266
- };
1267
- }
1268
- /**
1269
- * Update transaction
1270
- *
1271
- * @param transactionId - Transaction ID
1272
- * @param updates - Fields to update
1273
- * @returns Updated transaction
1274
- */
1275
- async update(transactionId, updates) {
1276
- const TransactionModel = this.models.Transaction;
1277
- const model = TransactionModel;
1278
- let transaction;
1279
- if (typeof model.update === "function") {
1280
- transaction = await model.update(transactionId, updates);
1281
- } else if (typeof model.findByIdAndUpdate === "function") {
1282
- transaction = await model.findByIdAndUpdate(
1283
- transactionId,
1284
- { $set: updates },
1285
- { new: true }
1286
- );
1287
- } else {
1288
- throw new Error("Transaction model does not support update operations");
1289
- }
1290
- if (!transaction) {
1291
- throw new TransactionNotFoundError(transactionId);
1292
- }
1293
- this._triggerHook("transaction.updated", {
1294
- transaction,
1295
- updates
1296
- });
1297
- return transaction;
1298
- }
1299
- /**
1300
- * Trigger event hook (fire-and-forget, non-blocking)
1301
- * @private
1302
- */
1303
- _triggerHook(event, data) {
1304
- triggerHook(this.hooks, event, data, this.logger);
1305
- }
1306
- };
1307
-
1308
- // src/enums/escrow.enums.ts
1309
- var HOLD_STATUS = {
1310
- PENDING: "pending",
1311
- HELD: "held",
1312
- RELEASED: "released",
1313
- CANCELLED: "cancelled",
1314
- EXPIRED: "expired",
1315
- PARTIALLY_RELEASED: "partially_released"
1316
- };
1317
- var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
1318
- var RELEASE_REASON = {
1319
- PAYMENT_VERIFIED: "payment_verified",
1320
- MANUAL_RELEASE: "manual_release",
1321
- AUTO_RELEASE: "auto_release",
1322
- DISPUTE_RESOLVED: "dispute_resolved"
1323
- };
1324
- var RELEASE_REASON_VALUES = Object.values(
1325
- RELEASE_REASON
1326
- );
1327
- var HOLD_REASON = {
1328
- PAYMENT_VERIFICATION: "payment_verification",
1329
- FRAUD_CHECK: "fraud_check",
1330
- MANUAL_REVIEW: "manual_review",
1331
- DISPUTE: "dispute",
1332
- COMPLIANCE: "compliance"
1333
- };
1334
- var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
1335
- new Set(HOLD_STATUS_VALUES);
1336
- new Set(RELEASE_REASON_VALUES);
1337
- new Set(HOLD_REASON_VALUES);
1338
-
1339
- // src/enums/split.enums.ts
1340
- var SPLIT_TYPE = {
1341
- PLATFORM_COMMISSION: "platform_commission",
1342
- AFFILIATE_COMMISSION: "affiliate_commission",
1343
- REFERRAL_COMMISSION: "referral_commission",
1344
- PARTNER_COMMISSION: "partner_commission",
1345
- CUSTOM: "custom"
1346
- };
1347
- var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
1348
- var SPLIT_STATUS = {
1349
- PENDING: "pending",
1350
- DUE: "due",
1351
- PAID: "paid",
1352
- WAIVED: "waived",
1353
- CANCELLED: "cancelled"
1354
- };
1355
- var SPLIT_STATUS_VALUES = Object.values(
1356
- SPLIT_STATUS
1357
- );
1358
- var PAYOUT_METHOD = {
1359
- BANK_TRANSFER: "bank_transfer",
1360
- MOBILE_WALLET: "mobile_wallet",
1361
- PLATFORM_BALANCE: "platform_balance",
1362
- CRYPTO: "crypto",
1363
- CHECK: "check",
1364
- MANUAL: "manual"
1365
- };
1366
- var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
1367
- new Set(SPLIT_TYPE_VALUES);
1368
- new Set(SPLIT_STATUS_VALUES);
1369
- new Set(PAYOUT_METHOD_VALUES);
1370
-
1371
- // src/utils/commission-split.ts
1372
- function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
1373
- if (!splitRules || splitRules.length === 0) {
1374
- return [];
1375
- }
1376
- if (amount < 0) {
1377
- throw new Error("Transaction amount cannot be negative");
1378
- }
1379
- if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
1380
- throw new Error("Gateway fee rate must be between 0 and 1");
1381
- }
1382
- const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
1383
- if (totalRate > 1) {
1384
- throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
1385
- }
1386
- return splitRules.map((rule, index) => {
1387
- if (rule.rate < 0 || rule.rate > 1) {
1388
- throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
1389
- }
1390
- const grossAmount = Math.round(amount * rule.rate * 100) / 100;
1391
- const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
1392
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
1393
- return {
1394
- type: rule.type ?? SPLIT_TYPE.CUSTOM,
1395
- recipientId: rule.recipientId,
1396
- recipientType: rule.recipientType,
1397
- rate: rule.rate,
1398
- grossAmount,
1399
- gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
1400
- gatewayFeeAmount,
1401
- netAmount,
1402
- status: SPLIT_STATUS.PENDING,
1403
- dueDate: rule.dueDate ?? null,
1404
- metadata: rule.metadata ?? {}
1405
- };
1406
- });
1407
- }
1408
- function calculateOrganizationPayout(amount, splits = []) {
1409
- const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
1410
- return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
1411
- }
1412
-
1413
- // src/services/escrow.service.ts
1414
- var EscrowService = class {
1415
- models;
1416
- hooks;
1417
- logger;
1418
- constructor(container) {
1419
- this.models = container.get("models");
1420
- this.hooks = container.get("hooks");
1421
- this.logger = container.get("logger");
1422
- }
1423
- /**
1424
- * Hold funds in escrow
1425
- *
1426
- * @param transactionId - Transaction to hold
1427
- * @param options - Hold options
1428
- * @returns Updated transaction
1429
- */
1430
- async hold(transactionId, options = {}) {
1431
- const {
1432
- reason = HOLD_REASON.PAYMENT_VERIFICATION,
1433
- holdUntil = null,
1434
- metadata = {}
1435
- } = options;
1436
- const TransactionModel = this.models.Transaction;
1437
- const transaction = await TransactionModel.findById(transactionId);
1438
- if (!transaction) {
1439
- throw new TransactionNotFoundError(transactionId);
1440
- }
1441
- if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
1442
- throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
1443
- }
1444
- transaction.hold = {
1445
- status: HOLD_STATUS.HELD,
1446
- heldAmount: transaction.amount,
1447
- releasedAmount: 0,
1448
- reason,
1449
- heldAt: /* @__PURE__ */ new Date(),
1450
- ...holdUntil && { holdUntil },
1451
- releases: [],
1452
- metadata
1453
- };
1454
- await transaction.save();
1455
- this._triggerHook("escrow.held", {
1456
- transaction,
1457
- heldAmount: transaction.amount,
1458
- reason
1459
- });
1460
- return transaction;
1461
- }
1462
- /**
1463
- * Release funds from escrow to recipient
1464
- *
1465
- * @param transactionId - Transaction to release
1466
- * @param options - Release options
1467
- * @returns { transaction, releaseTransaction }
1468
- */
1469
- async release(transactionId, options) {
1470
- const {
1471
- amount = null,
1472
- recipientId,
1473
- recipientType = "organization",
1474
- reason = RELEASE_REASON.PAYMENT_VERIFIED,
1475
- releasedBy = null,
1476
- createTransaction = true,
1477
- metadata = {}
1478
- } = options;
1479
- const TransactionModel = this.models.Transaction;
1480
- const transaction = await TransactionModel.findById(transactionId);
1481
- if (!transaction) {
1482
- throw new TransactionNotFoundError(transactionId);
1483
- }
1484
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
1485
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
1486
- }
1487
- if (!recipientId) {
1488
- throw new Error("recipientId is required for release");
1489
- }
1490
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
1491
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
1492
- if (releaseAmount > availableAmount) {
1493
- throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
1494
- }
1495
- const releaseRecord = {
1496
- amount: releaseAmount,
1497
- recipientId,
1498
- recipientType,
1499
- releasedAt: /* @__PURE__ */ new Date(),
1500
- releasedBy,
1501
- reason,
1502
- metadata
1503
- };
1504
- transaction.hold.releases.push(releaseRecord);
1505
- transaction.hold.releasedAmount += releaseAmount;
1506
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
1507
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
1508
- if (isFullRelease) {
1509
- transaction.hold.status = HOLD_STATUS.RELEASED;
1510
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
1511
- transaction.status = TRANSACTION_STATUS.COMPLETED;
1512
- } else if (isPartialRelease) {
1513
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
1514
- }
1515
- await transaction.save();
1516
- let releaseTransaction = null;
1517
- if (createTransaction) {
1518
- releaseTransaction = await TransactionModel.create({
1519
- organizationId: transaction.organizationId,
1520
- customerId: recipientId,
1521
- amount: releaseAmount,
1522
- currency: transaction.currency,
1523
- category: transaction.category,
1524
- type: TRANSACTION_TYPE.INCOME,
1525
- method: transaction.method,
1526
- status: TRANSACTION_STATUS.COMPLETED,
1527
- gateway: transaction.gateway,
1528
- referenceId: transaction.referenceId,
1529
- referenceModel: transaction.referenceModel,
1530
- metadata: {
1531
- ...metadata,
1532
- isRelease: true,
1533
- heldTransactionId: transaction._id.toString(),
1534
- releaseReason: reason,
1535
- recipientType
1536
- },
1537
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
1538
- });
1539
- }
1540
- this._triggerHook("escrow.released", {
1541
- transaction,
1542
- releaseTransaction,
1543
- releaseAmount,
1544
- recipientId,
1545
- recipientType,
1546
- reason,
1547
- isFullRelease,
1548
- isPartialRelease
1549
- });
1550
- return {
1551
- transaction,
1552
- releaseTransaction,
1553
- releaseAmount,
1554
- isFullRelease,
1555
- isPartialRelease
1556
- };
1557
- }
1558
- /**
1559
- * Cancel hold and release back to customer
1560
- *
1561
- * @param transactionId - Transaction to cancel hold
1562
- * @param options - Cancel options
1563
- * @returns Updated transaction
1564
- */
1565
- async cancel(transactionId, options = {}) {
1566
- const { reason = "Hold cancelled", metadata = {} } = options;
1567
- const TransactionModel = this.models.Transaction;
1568
- const transaction = await TransactionModel.findById(transactionId);
1569
- if (!transaction) {
1570
- throw new TransactionNotFoundError(transactionId);
1571
- }
1572
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
1573
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
1574
- }
1575
- transaction.hold.status = HOLD_STATUS.CANCELLED;
1576
- transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
1577
- transaction.hold.metadata = {
1578
- ...transaction.hold.metadata,
1579
- ...metadata,
1580
- cancelReason: reason
1581
- };
1582
- transaction.status = TRANSACTION_STATUS.CANCELLED;
1583
- await transaction.save();
1584
- this._triggerHook("escrow.cancelled", {
1585
- transaction,
1586
- reason
1587
- });
1588
- return transaction;
1589
- }
1590
- /**
1591
- * Split payment to multiple recipients
1592
- * Deducts splits from held amount and releases remainder to organization
1593
- *
1594
- * @param transactionId - Transaction to split
1595
- * @param splitRules - Split configuration
1596
- * @returns { transaction, splitTransactions, organizationTransaction }
1597
- */
1598
- async split(transactionId, splitRules = []) {
1599
- const TransactionModel = this.models.Transaction;
1600
- const transaction = await TransactionModel.findById(transactionId);
1601
- if (!transaction) {
1602
- throw new TransactionNotFoundError(transactionId);
1603
- }
1604
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
1605
- throw new Error(`Transaction must be held before splitting. Current: ${transaction.hold?.status ?? "none"}`);
1606
- }
1607
- if (!splitRules || splitRules.length === 0) {
1608
- throw new Error("splitRules cannot be empty");
1609
- }
1610
- const splits = calculateSplits(
1611
- transaction.amount,
1612
- splitRules,
1613
- transaction.commission?.gatewayFeeRate ?? 0
1614
- );
1615
- transaction.splits = splits;
1616
- await transaction.save();
1617
- const splitTransactions = [];
1618
- for (const split of splits) {
1619
- const splitTransaction = await TransactionModel.create({
1620
- organizationId: transaction.organizationId,
1621
- customerId: split.recipientId,
1622
- amount: split.netAmount,
1623
- currency: transaction.currency,
1624
- category: split.type,
1625
- type: TRANSACTION_TYPE.EXPENSE,
1626
- method: transaction.method,
1627
- status: TRANSACTION_STATUS.COMPLETED,
1628
- gateway: transaction.gateway,
1629
- referenceId: transaction.referenceId,
1630
- referenceModel: transaction.referenceModel,
1631
- metadata: {
1632
- isSplit: true,
1633
- splitType: split.type,
1634
- recipientType: split.recipientType,
1635
- originalTransactionId: transaction._id.toString(),
1636
- grossAmount: split.grossAmount,
1637
- gatewayFeeAmount: split.gatewayFeeAmount
1638
- },
1639
- idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
1640
- });
1641
- split.payoutTransactionId = splitTransaction._id.toString();
1642
- split.status = SPLIT_STATUS.PAID;
1643
- split.paidDate = /* @__PURE__ */ new Date();
1644
- splitTransactions.push(splitTransaction);
1645
- }
1646
- await transaction.save();
1647
- const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
1648
- const organizationTransaction = await this.release(transactionId, {
1649
- amount: organizationPayout,
1650
- recipientId: transaction.organizationId?.toString() ?? "",
1651
- recipientType: "organization",
1652
- reason: RELEASE_REASON.PAYMENT_VERIFIED,
1653
- createTransaction: true,
1654
- metadata: {
1655
- afterSplits: true,
1656
- totalSplits: splits.length,
1657
- totalSplitAmount: transaction.amount - organizationPayout
1658
- }
1659
- });
1660
- this._triggerHook("escrow.split", {
1661
- transaction,
1662
- splits,
1663
- splitTransactions,
1664
- organizationTransaction: organizationTransaction.releaseTransaction,
1665
- organizationPayout
1666
- });
1667
- return {
1668
- transaction,
1669
- splits,
1670
- splitTransactions,
1671
- organizationTransaction: organizationTransaction.releaseTransaction,
1672
- organizationPayout
1673
- };
1674
- }
1675
- /**
1676
- * Get escrow status
1677
- *
1678
- * @param transactionId - Transaction ID
1679
- * @returns Escrow status
1680
- */
1681
- async getStatus(transactionId) {
1682
- const TransactionModel = this.models.Transaction;
1683
- const transaction = await TransactionModel.findById(transactionId);
1684
- if (!transaction) {
1685
- throw new TransactionNotFoundError(transactionId);
1686
- }
1687
- return {
1688
- transaction,
1689
- hold: transaction.hold ?? null,
1690
- splits: transaction.splits ?? [],
1691
- hasHold: !!transaction.hold,
1692
- hasSplits: transaction.splits ? transaction.splits.length > 0 : false
1693
- };
1694
- }
1695
- _triggerHook(event, data) {
1696
- triggerHook(this.hooks, event, data, this.logger);
1697
- }
1698
- };
1699
-
1700
- export { EscrowService, MonetizationService, PaymentService, TransactionService };
1701
- //# sourceMappingURL=index.js.map
1702
- //# sourceMappingURL=index.js.map