@classytic/revenue 0.2.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +498 -501
  2. package/dist/actions-CwG-b7fR.d.ts +519 -0
  3. package/dist/core/index.d.ts +884 -0
  4. package/dist/core/index.js +2941 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/enums/index.d.ts +130 -0
  7. package/dist/enums/index.js +167 -0
  8. package/dist/enums/index.js.map +1 -0
  9. package/dist/index-BnJWVXuw.d.ts +378 -0
  10. package/dist/index-ChVD3P9k.d.ts +504 -0
  11. package/dist/index.d.ts +42 -0
  12. package/dist/index.js +4353 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/providers/index.d.ts +132 -0
  15. package/dist/providers/index.js +122 -0
  16. package/dist/providers/index.js.map +1 -0
  17. package/dist/retry-80lBCmSe.d.ts +234 -0
  18. package/dist/schemas/index.d.ts +894 -0
  19. package/dist/schemas/index.js +524 -0
  20. package/dist/schemas/index.js.map +1 -0
  21. package/dist/schemas/validation.d.ts +309 -0
  22. package/dist/schemas/validation.js +249 -0
  23. package/dist/schemas/validation.js.map +1 -0
  24. package/dist/services/index.d.ts +3 -0
  25. package/dist/services/index.js +1632 -0
  26. package/dist/services/index.js.map +1 -0
  27. package/dist/split.enums-DHdM1YAV.d.ts +255 -0
  28. package/dist/split.schema-BPdFZMbU.d.ts +958 -0
  29. package/dist/utils/index.d.ts +24 -0
  30. package/dist/utils/index.js +1067 -0
  31. package/dist/utils/index.js.map +1 -0
  32. package/package.json +48 -32
  33. package/core/builder.js +0 -219
  34. package/core/container.js +0 -119
  35. package/core/errors.js +0 -262
  36. package/dist/types/core/builder.d.ts +0 -97
  37. package/dist/types/core/container.d.ts +0 -57
  38. package/dist/types/core/errors.d.ts +0 -122
  39. package/dist/types/enums/escrow.enums.d.ts +0 -24
  40. package/dist/types/enums/index.d.ts +0 -69
  41. package/dist/types/enums/monetization.enums.d.ts +0 -6
  42. package/dist/types/enums/payment.enums.d.ts +0 -16
  43. package/dist/types/enums/split.enums.d.ts +0 -25
  44. package/dist/types/enums/subscription.enums.d.ts +0 -15
  45. package/dist/types/enums/transaction.enums.d.ts +0 -24
  46. package/dist/types/index.d.ts +0 -22
  47. package/dist/types/providers/base.d.ts +0 -128
  48. package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
  49. package/dist/types/schemas/escrow/index.d.ts +0 -6
  50. package/dist/types/schemas/index.d.ts +0 -506
  51. package/dist/types/schemas/split/index.d.ts +0 -8
  52. package/dist/types/schemas/split/split.schema.d.ts +0 -142
  53. package/dist/types/schemas/subscription/index.d.ts +0 -152
  54. package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
  55. package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
  56. package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
  57. package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
  58. package/dist/types/schemas/transaction/index.d.ts +0 -202
  59. package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
  60. package/dist/types/services/escrow.service.d.ts +0 -51
  61. package/dist/types/services/monetization.service.d.ts +0 -193
  62. package/dist/types/services/payment.service.d.ts +0 -117
  63. package/dist/types/services/transaction.service.d.ts +0 -40
  64. package/dist/types/utils/category-resolver.d.ts +0 -46
  65. package/dist/types/utils/commission-split.d.ts +0 -56
  66. package/dist/types/utils/commission.d.ts +0 -29
  67. package/dist/types/utils/hooks.d.ts +0 -17
  68. package/dist/types/utils/index.d.ts +0 -6
  69. package/dist/types/utils/logger.d.ts +0 -12
  70. package/dist/types/utils/subscription/actions.d.ts +0 -28
  71. package/dist/types/utils/subscription/index.d.ts +0 -2
  72. package/dist/types/utils/subscription/period.d.ts +0 -47
  73. package/dist/types/utils/transaction-type.d.ts +0 -102
  74. package/enums/escrow.enums.js +0 -36
  75. package/enums/index.d.ts +0 -116
  76. package/enums/index.js +0 -110
  77. package/enums/monetization.enums.js +0 -15
  78. package/enums/payment.enums.js +0 -64
  79. package/enums/split.enums.js +0 -37
  80. package/enums/subscription.enums.js +0 -33
  81. package/enums/transaction.enums.js +0 -69
  82. package/index.js +0 -76
  83. package/providers/base.js +0 -162
  84. package/schemas/escrow/hold.schema.js +0 -62
  85. package/schemas/escrow/index.js +0 -15
  86. package/schemas/index.d.ts +0 -33
  87. package/schemas/index.js +0 -27
  88. package/schemas/split/index.js +0 -16
  89. package/schemas/split/split.schema.js +0 -86
  90. package/schemas/subscription/index.js +0 -17
  91. package/schemas/subscription/info.schema.js +0 -115
  92. package/schemas/subscription/plan.schema.js +0 -48
  93. package/schemas/transaction/common.schema.js +0 -22
  94. package/schemas/transaction/gateway.schema.js +0 -69
  95. package/schemas/transaction/index.js +0 -20
  96. package/schemas/transaction/payment.schema.js +0 -110
  97. package/services/escrow.service.js +0 -353
  98. package/services/monetization.service.js +0 -675
  99. package/services/payment.service.js +0 -535
  100. package/services/transaction.service.js +0 -142
  101. package/utils/category-resolver.js +0 -74
  102. package/utils/commission-split.js +0 -180
  103. package/utils/commission.js +0 -83
  104. package/utils/hooks.js +0 -44
  105. package/utils/index.d.ts +0 -164
  106. package/utils/index.js +0 -16
  107. package/utils/logger.js +0 -36
  108. package/utils/subscription/actions.js +0 -68
  109. package/utils/subscription/index.js +0 -20
  110. package/utils/subscription/period.js +0 -123
  111. package/utils/transaction-type.js +0 -254
@@ -1,675 +0,0 @@
1
- /**
2
- * Monetization Service
3
- * @classytic/revenue
4
- *
5
- * Framework-agnostic monetization management service with DI
6
- * Handles purchases, subscriptions, and free items using provider system
7
- */
8
-
9
- /**
10
- * @typedef {Object} MonetizationCreateParams
11
- * @property {Object} data - Monetization data
12
- * @property {string} [data.organizationId] - Organization ID (for multi-tenant)
13
- * @property {string} [data.customerId] - Customer ID
14
- * @property {string} [data.referenceId] - Reference to entity (Order, Subscription, etc.)
15
- * @property {string} [data.referenceModel] - Model name for reference
16
- * @property {string} planKey - Plan key ('monthly', 'quarterly', 'yearly', 'one_time', etc.)
17
- * @property {number} amount - Amount (0 for free)
18
- * @property {string} [currency='BDT'] - Currency code
19
- * @property {string} [gateway='manual'] - Payment gateway name
20
- * @property {string} [entity] - Logical entity identifier
21
- * @property {'free'|'subscription'|'purchase'} [monetizationType='subscription'] - Monetization type
22
- * @property {Object} [paymentData] - Payment method details
23
- * @property {Object} [metadata] - Additional metadata
24
- * @property {string} [idempotencyKey] - Idempotency key
25
- */
26
-
27
- /**
28
- * @typedef {Object} MonetizationCreateResult
29
- * @property {Object|null} subscription - Subscription record (if Subscription model exists)
30
- * @property {Object|null} transaction - Transaction record
31
- * @property {Object|null} paymentIntent - Payment intent from provider
32
- */
33
-
34
- import { nanoid } from 'nanoid';
35
- import {
36
- MissingRequiredFieldError,
37
- InvalidAmountError,
38
- ProviderNotFoundError,
39
- SubscriptionNotFoundError,
40
- ModelNotRegisteredError,
41
- SubscriptionNotActiveError,
42
- PaymentIntentCreationError,
43
- InvalidStateTransitionError,
44
- } from '../core/errors.js';
45
- import { triggerHook } from '../utils/hooks.js';
46
- import { resolveCategory } from '../utils/category-resolver.js';
47
- import { calculateCommission } from '../utils/commission.js';
48
- import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
49
- import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
50
-
51
- /**
52
- * Monetization Service
53
- * Uses DI container for all dependencies
54
- */
55
- export class MonetizationService {
56
- constructor(container) {
57
- this.container = container;
58
- this.models = container.get('models');
59
- this.providers = container.get('providers');
60
- this.config = container.get('config');
61
- this.hooks = container.get('hooks');
62
- this.logger = container.get('logger');
63
- }
64
-
65
- /**
66
- * Create a new monetization (purchase, subscription, or free item)
67
- *
68
- * @param {MonetizationCreateParams} params - Monetization parameters
69
- *
70
- * @example
71
- * // One-time purchase
72
- * await revenue.monetization.create({
73
- * data: {
74
- * organizationId: '...',
75
- * customerId: '...',
76
- * referenceId: order._id,
77
- * referenceModel: 'Order',
78
- * },
79
- * planKey: 'one_time',
80
- * monetizationType: 'purchase',
81
- * gateway: 'bkash',
82
- * amount: 1500,
83
- * });
84
- *
85
- * // Recurring subscription
86
- * await revenue.monetization.create({
87
- * data: {
88
- * organizationId: '...',
89
- * customerId: '...',
90
- * referenceId: subscription._id,
91
- * referenceModel: 'Subscription',
92
- * },
93
- * planKey: 'monthly',
94
- * monetizationType: 'subscription',
95
- * gateway: 'stripe',
96
- * amount: 2000,
97
- * });
98
- *
99
- * @returns {Promise<MonetizationCreateResult>} Result with subscription, transaction, and paymentIntent
100
- */
101
- async create(params) {
102
- const {
103
- data,
104
- planKey,
105
- amount,
106
- currency = 'BDT',
107
- gateway = 'manual',
108
- entity = null,
109
- monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
110
- paymentData,
111
- metadata = {},
112
- idempotencyKey = null,
113
- } = params;
114
-
115
- // Validate required fields
116
- // Note: organizationId is OPTIONAL (only needed for multi-tenant)
117
-
118
- if (!planKey) {
119
- throw new MissingRequiredFieldError('planKey');
120
- }
121
-
122
- if (amount < 0) {
123
- throw new InvalidAmountError(amount);
124
- }
125
-
126
- const isFree = amount === 0;
127
-
128
- // Get provider
129
- const provider = this.providers[gateway];
130
- if (!provider) {
131
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
132
- }
133
-
134
- // Create payment intent if not free
135
- let paymentIntent = null;
136
- let transaction = null;
137
-
138
- if (!isFree) {
139
- // Create payment intent via provider
140
- try {
141
- paymentIntent = await provider.createIntent({
142
- amount,
143
- currency,
144
- metadata: {
145
- ...metadata,
146
- type: 'subscription',
147
- planKey,
148
- },
149
- });
150
- } catch (error) {
151
- throw new PaymentIntentCreationError(gateway, error);
152
- }
153
-
154
- // Resolve category based on entity and monetizationType
155
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
156
-
157
- // Resolve transaction type using config mapping or default to 'income'
158
- const transactionType = this.config.transactionTypeMapping?.subscription
159
- || this.config.transactionTypeMapping?.[monetizationType]
160
- || TRANSACTION_TYPE.INCOME;
161
-
162
- // Calculate commission if configured
163
- const commissionRate = this.config.commissionRates?.[category] || 0;
164
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] || 0;
165
- const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
166
-
167
- // Create transaction record
168
- const TransactionModel = this.models.Transaction;
169
- transaction = await TransactionModel.create({
170
- organizationId: data.organizationId,
171
- customerId: data.customerId || null,
172
- amount,
173
- currency,
174
- category,
175
- type: transactionType,
176
- method: paymentData?.method || 'manual',
177
- status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
178
- gateway: {
179
- type: gateway,
180
- sessionId: paymentIntent.sessionId,
181
- paymentIntentId: paymentIntent.paymentIntentId,
182
- provider: paymentIntent.provider,
183
- metadata: paymentIntent.metadata,
184
- },
185
- paymentDetails: {
186
- provider: gateway,
187
- ...paymentData,
188
- },
189
- ...(commission && { commission }), // Only include if commission exists
190
- // Polymorphic reference (top-level, not metadata)
191
- ...(data.referenceId && { referenceId: data.referenceId }),
192
- ...(data.referenceModel && { referenceModel: data.referenceModel }),
193
- metadata: {
194
- ...metadata,
195
- planKey,
196
- entity,
197
- monetizationType,
198
- paymentIntentId: paymentIntent.id,
199
- },
200
- idempotencyKey: idempotencyKey || `sub_${nanoid(16)}`,
201
- });
202
- }
203
-
204
- // Create subscription record (if Subscription model exists)
205
- let subscription = null;
206
- if (this.models.Subscription) {
207
- const SubscriptionModel = this.models.Subscription;
208
-
209
- // Create subscription with proper reference tracking
210
- const subscriptionData = {
211
- organizationId: data.organizationId,
212
- customerId: data.customerId || null,
213
- planKey,
214
- amount,
215
- currency,
216
- status: isFree ? 'active' : 'pending',
217
- isActive: isFree,
218
- gateway,
219
- transactionId: transaction?._id || null,
220
- paymentIntentId: paymentIntent?.id || null,
221
- metadata: {
222
- ...metadata,
223
- isFree,
224
- entity,
225
- monetizationType,
226
- },
227
- ...data,
228
- };
229
-
230
- // Remove referenceId/referenceModel from subscription (they're for transactions)
231
- delete subscriptionData.referenceId;
232
- delete subscriptionData.referenceModel;
233
-
234
- subscription = await SubscriptionModel.create(subscriptionData);
235
- }
236
-
237
- // Trigger hooks - emit specific event based on monetization type
238
- const eventData = {
239
- subscription,
240
- transaction,
241
- paymentIntent,
242
- isFree,
243
- monetizationType,
244
- };
245
-
246
- // Emit specific monetization event
247
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
248
- this._triggerHook('purchase.created', eventData);
249
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
250
- this._triggerHook('subscription.created', eventData);
251
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
252
- this._triggerHook('free.created', eventData);
253
- }
254
-
255
- // Also emit generic event for backward compatibility
256
- this._triggerHook('monetization.created', eventData);
257
-
258
- return {
259
- subscription,
260
- transaction,
261
- paymentIntent,
262
- };
263
- }
264
-
265
- /**
266
- * Activate subscription after payment verification
267
- *
268
- * @param {String} subscriptionId - Subscription ID or transaction ID
269
- * @param {Object} options - Activation options
270
- * @returns {Promise<Object>} Updated subscription
271
- */
272
- async activate(subscriptionId, options = {}) {
273
- const { timestamp = new Date() } = options;
274
-
275
- if (!this.models.Subscription) {
276
- throw new ModelNotRegisteredError('Subscription');
277
- }
278
-
279
- const SubscriptionModel = this.models.Subscription;
280
- const subscription = await SubscriptionModel.findById(subscriptionId);
281
-
282
- if (!subscription) {
283
- throw new SubscriptionNotFoundError(subscriptionId);
284
- }
285
-
286
- if (subscription.isActive) {
287
- this.logger.warn('Subscription already active', { subscriptionId });
288
- return subscription;
289
- }
290
-
291
- // Calculate period dates based on plan
292
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
293
-
294
- // Update subscription
295
- subscription.isActive = true;
296
- subscription.status = 'active';
297
- subscription.startDate = timestamp;
298
- subscription.endDate = periodEnd;
299
- subscription.activatedAt = timestamp;
300
-
301
- await subscription.save();
302
-
303
- // Trigger hook
304
- this._triggerHook('subscription.activated', {
305
- subscription,
306
- activatedAt: timestamp,
307
- });
308
-
309
- return subscription;
310
- }
311
-
312
- /**
313
- * Renew subscription
314
- *
315
- * @param {String} subscriptionId - Subscription ID
316
- * @param {Object} params - Renewal parameters
317
- * @param {String} params.gateway - Payment gateway name (default: 'manual') - Use ANY registered provider name: 'manual', 'bkash', 'nagad', 'stripe', etc.
318
- * @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
319
- * @param {Object} params.paymentData - Payment method details
320
- * @param {Object} params.metadata - Additional metadata
321
- * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
322
- * @returns {Promise<Object>} { subscription, transaction, paymentIntent }
323
- */
324
- async renew(subscriptionId, params = {}) {
325
- const {
326
- gateway = 'manual',
327
- entity = null,
328
- paymentData,
329
- metadata = {},
330
- idempotencyKey = null,
331
- } = params;
332
-
333
- if (!this.models.Subscription) {
334
- throw new ModelNotRegisteredError('Subscription');
335
- }
336
-
337
- const SubscriptionModel = this.models.Subscription;
338
- const subscription = await SubscriptionModel.findById(subscriptionId);
339
-
340
- if (!subscription) {
341
- throw new SubscriptionNotFoundError(subscriptionId);
342
- }
343
-
344
- if (subscription.amount === 0) {
345
- throw new InvalidAmountError(0, 'Free subscriptions do not require renewal');
346
- }
347
-
348
- // Get provider
349
- const provider = this.providers[gateway];
350
- if (!provider) {
351
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
352
- }
353
-
354
- // Create payment intent
355
- let paymentIntent = null;
356
- try {
357
- paymentIntent = await provider.createIntent({
358
- amount: subscription.amount,
359
- currency: subscription.currency || 'BDT',
360
- metadata: {
361
- ...metadata,
362
- type: 'subscription_renewal',
363
- subscriptionId: subscription._id.toString(),
364
- },
365
- });
366
- } catch (error) {
367
- this.logger.error('Failed to create payment intent for renewal:', error);
368
- throw new PaymentIntentCreationError(gateway, error);
369
- }
370
-
371
- // Resolve category - use provided entity or inherit from subscription metadata
372
- const effectiveEntity = entity || subscription.metadata?.entity;
373
- const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
374
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
375
-
376
- // Resolve transaction type using config mapping or default to 'income'
377
- const transactionType = this.config.transactionTypeMapping?.subscription_renewal
378
- || this.config.transactionTypeMapping?.subscription
379
- || this.config.transactionTypeMapping?.[effectiveMonetizationType]
380
- || TRANSACTION_TYPE.INCOME;
381
-
382
- // Calculate commission if configured
383
- const commissionRate = this.config.commissionRates?.[category] || 0;
384
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] || 0;
385
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
386
-
387
- // Create transaction
388
- const TransactionModel = this.models.Transaction;
389
- const transaction = await TransactionModel.create({
390
- organizationId: subscription.organizationId,
391
- customerId: subscription.customerId,
392
- amount: subscription.amount,
393
- currency: subscription.currency || 'BDT',
394
- category,
395
- type: transactionType,
396
- method: paymentData?.method || 'manual',
397
- status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
398
- gateway: {
399
- type: gateway,
400
- sessionId: paymentIntent.sessionId,
401
- paymentIntentId: paymentIntent.paymentIntentId,
402
- provider: paymentIntent.provider,
403
- metadata: paymentIntent.metadata,
404
- },
405
- paymentDetails: {
406
- provider: gateway,
407
- ...paymentData,
408
- },
409
- ...(commission && { commission }), // Only include if commission exists
410
- // Polymorphic reference to subscription
411
- referenceId: subscription._id,
412
- referenceModel: 'Subscription',
413
- metadata: {
414
- ...metadata,
415
- subscriptionId: subscription._id.toString(), // Keep for backward compat
416
- entity: effectiveEntity,
417
- monetizationType: effectiveMonetizationType,
418
- isRenewal: true,
419
- paymentIntentId: paymentIntent.id,
420
- },
421
- idempotencyKey: idempotencyKey || `renewal_${nanoid(16)}`,
422
- });
423
-
424
- // Update subscription
425
- subscription.status = 'pending_renewal';
426
- subscription.renewalTransactionId = transaction._id;
427
- subscription.renewalCount = (subscription.renewalCount || 0) + 1;
428
- await subscription.save();
429
-
430
- // Trigger hook
431
- this._triggerHook('subscription.renewed', {
432
- subscription,
433
- transaction,
434
- paymentIntent,
435
- renewalCount: subscription.renewalCount,
436
- });
437
-
438
- return {
439
- subscription,
440
- transaction,
441
- paymentIntent,
442
- };
443
- }
444
-
445
- /**
446
- * Cancel subscription
447
- *
448
- * @param {String} subscriptionId - Subscription ID
449
- * @param {Object} options - Cancellation options
450
- * @param {Boolean} options.immediate - Cancel immediately vs at period end
451
- * @param {String} options.reason - Cancellation reason
452
- * @returns {Promise<Object>} Updated subscription
453
- */
454
- async cancel(subscriptionId, options = {}) {
455
- const { immediate = false, reason = null } = options;
456
-
457
- if (!this.models.Subscription) {
458
- throw new ModelNotRegisteredError('Subscription');
459
- }
460
-
461
- const SubscriptionModel = this.models.Subscription;
462
- const subscription = await SubscriptionModel.findById(subscriptionId);
463
-
464
- if (!subscription) {
465
- throw new SubscriptionNotFoundError(subscriptionId);
466
- }
467
-
468
- const now = new Date();
469
-
470
- if (immediate) {
471
- subscription.isActive = false;
472
- subscription.status = 'cancelled';
473
- subscription.canceledAt = now;
474
- subscription.cancellationReason = reason;
475
- } else {
476
- // Schedule cancellation at period end
477
- subscription.cancelAt = subscription.endDate || now;
478
- subscription.cancellationReason = reason;
479
- }
480
-
481
- await subscription.save();
482
-
483
- // Trigger hook
484
- this._triggerHook('subscription.cancelled', {
485
- subscription,
486
- immediate,
487
- reason,
488
- canceledAt: immediate ? now : subscription.cancelAt,
489
- });
490
-
491
- return subscription;
492
- }
493
-
494
- /**
495
- * Pause subscription
496
- *
497
- * @param {String} subscriptionId - Subscription ID
498
- * @param {Object} options - Pause options
499
- * @returns {Promise<Object>} Updated subscription
500
- */
501
- async pause(subscriptionId, options = {}) {
502
- const { reason = null } = options;
503
-
504
- if (!this.models.Subscription) {
505
- throw new ModelNotRegisteredError('Subscription');
506
- }
507
-
508
- const SubscriptionModel = this.models.Subscription;
509
- const subscription = await SubscriptionModel.findById(subscriptionId);
510
-
511
- if (!subscription) {
512
- throw new SubscriptionNotFoundError(subscriptionId);
513
- }
514
-
515
- if (!subscription.isActive) {
516
- throw new SubscriptionNotActiveError(subscriptionId, 'Only active subscriptions can be paused');
517
- }
518
-
519
- const pausedAt = new Date();
520
- subscription.isActive = false;
521
- subscription.status = 'paused';
522
- subscription.pausedAt = pausedAt;
523
- subscription.pauseReason = reason;
524
-
525
- await subscription.save();
526
-
527
- // Trigger hook
528
- this._triggerHook('subscription.paused', {
529
- subscription,
530
- reason,
531
- pausedAt,
532
- });
533
-
534
- return subscription;
535
- }
536
-
537
- /**
538
- * Resume subscription
539
- *
540
- * @param {String} subscriptionId - Subscription ID
541
- * @param {Object} options - Resume options
542
- * @returns {Promise<Object>} Updated subscription
543
- */
544
- async resume(subscriptionId, options = {}) {
545
- const { extendPeriod = false } = options;
546
-
547
- if (!this.models.Subscription) {
548
- throw new ModelNotRegisteredError('Subscription');
549
- }
550
-
551
- const SubscriptionModel = this.models.Subscription;
552
- const subscription = await SubscriptionModel.findById(subscriptionId);
553
-
554
- if (!subscription) {
555
- throw new SubscriptionNotFoundError(subscriptionId);
556
- }
557
-
558
- if (!subscription.pausedAt) {
559
- throw new InvalidStateTransitionError(
560
- 'resume',
561
- 'paused',
562
- subscription.status,
563
- 'Only paused subscriptions can be resumed'
564
- );
565
- }
566
-
567
- const now = new Date();
568
- const pausedAt = new Date(subscription.pausedAt);
569
- const pauseDuration = now - pausedAt;
570
-
571
- subscription.isActive = true;
572
- subscription.status = 'active';
573
- subscription.pausedAt = null;
574
- subscription.pauseReason = null;
575
-
576
- // Optionally extend period by pause duration
577
- if (extendPeriod && subscription.endDate) {
578
- const currentEnd = new Date(subscription.endDate);
579
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
580
- }
581
-
582
- await subscription.save();
583
-
584
- // Trigger hook
585
- this._triggerHook('subscription.resumed', {
586
- subscription,
587
- extendPeriod,
588
- pauseDuration,
589
- resumedAt: now,
590
- });
591
-
592
- return subscription;
593
- }
594
-
595
- /**
596
- * List subscriptions with filters
597
- *
598
- * @param {Object} filters - Query filters
599
- * @param {Object} options - Query options (limit, skip, sort)
600
- * @returns {Promise<Array>} Subscriptions
601
- */
602
- async list(filters = {}, options = {}) {
603
- if (!this.models.Subscription) {
604
- throw new ModelNotRegisteredError('Subscription');
605
- }
606
-
607
- const SubscriptionModel = this.models.Subscription;
608
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
609
-
610
- const subscriptions = await SubscriptionModel
611
- .find(filters)
612
- .limit(limit)
613
- .skip(skip)
614
- .sort(sort);
615
-
616
- return subscriptions;
617
- }
618
-
619
- /**
620
- * Get subscription by ID
621
- *
622
- * @param {String} subscriptionId - Subscription ID
623
- * @returns {Promise<Object>} Subscription
624
- */
625
- async get(subscriptionId) {
626
- if (!this.models.Subscription) {
627
- throw new ModelNotRegisteredError('Subscription');
628
- }
629
-
630
- const SubscriptionModel = this.models.Subscription;
631
- const subscription = await SubscriptionModel.findById(subscriptionId);
632
-
633
- if (!subscription) {
634
- throw new SubscriptionNotFoundError(subscriptionId);
635
- }
636
-
637
- return subscription;
638
- }
639
-
640
- /**
641
- * Calculate period end date based on plan key
642
- * @private
643
- */
644
- _calculatePeriodEnd(planKey, startDate = new Date()) {
645
- const start = new Date(startDate);
646
- let end = new Date(start);
647
-
648
- switch (planKey) {
649
- case 'monthly':
650
- end.setMonth(end.getMonth() + 1);
651
- break;
652
- case 'quarterly':
653
- end.setMonth(end.getMonth() + 3);
654
- break;
655
- case 'yearly':
656
- end.setFullYear(end.getFullYear() + 1);
657
- break;
658
- default:
659
- // Default to 30 days
660
- end.setDate(end.getDate() + 30);
661
- }
662
-
663
- return end;
664
- }
665
-
666
- /**
667
- * Trigger event hook (fire-and-forget, non-blocking)
668
- * @private
669
- */
670
- _triggerHook(event, data) {
671
- triggerHook(this.hooks, event, data, this.logger);
672
- }
673
- }
674
-
675
- export default MonetizationService;