@classytic/revenue 0.2.3 → 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 -499
  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 -126
  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 -112
  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 -671
  99. package/services/payment.service.js +0 -517
  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,671 +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
- paymentIntentId: paymentIntent.id,
181
- provider: paymentIntent.provider,
182
- },
183
- paymentDetails: {
184
- provider: gateway,
185
- ...paymentData,
186
- },
187
- ...(commission && { commission }), // Only include if commission exists
188
- // Polymorphic reference (top-level, not metadata)
189
- ...(data.referenceId && { referenceId: data.referenceId }),
190
- ...(data.referenceModel && { referenceModel: data.referenceModel }),
191
- metadata: {
192
- ...metadata,
193
- planKey,
194
- entity,
195
- monetizationType,
196
- paymentIntentId: paymentIntent.id,
197
- },
198
- idempotencyKey: idempotencyKey || `sub_${nanoid(16)}`,
199
- });
200
- }
201
-
202
- // Create subscription record (if Subscription model exists)
203
- let subscription = null;
204
- if (this.models.Subscription) {
205
- const SubscriptionModel = this.models.Subscription;
206
-
207
- // Create subscription with proper reference tracking
208
- const subscriptionData = {
209
- organizationId: data.organizationId,
210
- customerId: data.customerId || null,
211
- planKey,
212
- amount,
213
- currency,
214
- status: isFree ? 'active' : 'pending',
215
- isActive: isFree,
216
- gateway,
217
- transactionId: transaction?._id || null,
218
- paymentIntentId: paymentIntent?.id || null,
219
- metadata: {
220
- ...metadata,
221
- isFree,
222
- entity,
223
- monetizationType,
224
- },
225
- ...data,
226
- };
227
-
228
- // Remove referenceId/referenceModel from subscription (they're for transactions)
229
- delete subscriptionData.referenceId;
230
- delete subscriptionData.referenceModel;
231
-
232
- subscription = await SubscriptionModel.create(subscriptionData);
233
- }
234
-
235
- // Trigger hooks - emit specific event based on monetization type
236
- const eventData = {
237
- subscription,
238
- transaction,
239
- paymentIntent,
240
- isFree,
241
- monetizationType,
242
- };
243
-
244
- // Emit specific monetization event
245
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
246
- this._triggerHook('purchase.created', eventData);
247
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
248
- this._triggerHook('subscription.created', eventData);
249
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
250
- this._triggerHook('free.created', eventData);
251
- }
252
-
253
- // Also emit generic event for backward compatibility
254
- this._triggerHook('monetization.created', eventData);
255
-
256
- return {
257
- subscription,
258
- transaction,
259
- paymentIntent,
260
- };
261
- }
262
-
263
- /**
264
- * Activate subscription after payment verification
265
- *
266
- * @param {String} subscriptionId - Subscription ID or transaction ID
267
- * @param {Object} options - Activation options
268
- * @returns {Promise<Object>} Updated subscription
269
- */
270
- async activate(subscriptionId, options = {}) {
271
- const { timestamp = new Date() } = options;
272
-
273
- if (!this.models.Subscription) {
274
- throw new ModelNotRegisteredError('Subscription');
275
- }
276
-
277
- const SubscriptionModel = this.models.Subscription;
278
- const subscription = await SubscriptionModel.findById(subscriptionId);
279
-
280
- if (!subscription) {
281
- throw new SubscriptionNotFoundError(subscriptionId);
282
- }
283
-
284
- if (subscription.isActive) {
285
- this.logger.warn('Subscription already active', { subscriptionId });
286
- return subscription;
287
- }
288
-
289
- // Calculate period dates based on plan
290
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
291
-
292
- // Update subscription
293
- subscription.isActive = true;
294
- subscription.status = 'active';
295
- subscription.startDate = timestamp;
296
- subscription.endDate = periodEnd;
297
- subscription.activatedAt = timestamp;
298
-
299
- await subscription.save();
300
-
301
- // Trigger hook
302
- this._triggerHook('subscription.activated', {
303
- subscription,
304
- activatedAt: timestamp,
305
- });
306
-
307
- return subscription;
308
- }
309
-
310
- /**
311
- * Renew subscription
312
- *
313
- * @param {String} subscriptionId - Subscription ID
314
- * @param {Object} params - Renewal parameters
315
- * @param {String} params.gateway - Payment gateway name (default: 'manual') - Use ANY registered provider name: 'manual', 'bkash', 'nagad', 'stripe', etc.
316
- * @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
317
- * @param {Object} params.paymentData - Payment method details
318
- * @param {Object} params.metadata - Additional metadata
319
- * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
320
- * @returns {Promise<Object>} { subscription, transaction, paymentIntent }
321
- */
322
- async renew(subscriptionId, params = {}) {
323
- const {
324
- gateway = 'manual',
325
- entity = null,
326
- paymentData,
327
- metadata = {},
328
- idempotencyKey = null,
329
- } = params;
330
-
331
- if (!this.models.Subscription) {
332
- throw new ModelNotRegisteredError('Subscription');
333
- }
334
-
335
- const SubscriptionModel = this.models.Subscription;
336
- const subscription = await SubscriptionModel.findById(subscriptionId);
337
-
338
- if (!subscription) {
339
- throw new SubscriptionNotFoundError(subscriptionId);
340
- }
341
-
342
- if (subscription.amount === 0) {
343
- throw new InvalidAmountError(0, 'Free subscriptions do not require renewal');
344
- }
345
-
346
- // Get provider
347
- const provider = this.providers[gateway];
348
- if (!provider) {
349
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
350
- }
351
-
352
- // Create payment intent
353
- let paymentIntent = null;
354
- try {
355
- paymentIntent = await provider.createIntent({
356
- amount: subscription.amount,
357
- currency: subscription.currency || 'BDT',
358
- metadata: {
359
- ...metadata,
360
- type: 'subscription_renewal',
361
- subscriptionId: subscription._id.toString(),
362
- },
363
- });
364
- } catch (error) {
365
- this.logger.error('Failed to create payment intent for renewal:', error);
366
- throw new PaymentIntentCreationError(gateway, error);
367
- }
368
-
369
- // Resolve category - use provided entity or inherit from subscription metadata
370
- const effectiveEntity = entity || subscription.metadata?.entity;
371
- const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
372
- const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
373
-
374
- // Resolve transaction type using config mapping or default to 'income'
375
- const transactionType = this.config.transactionTypeMapping?.subscription_renewal
376
- || this.config.transactionTypeMapping?.subscription
377
- || this.config.transactionTypeMapping?.[effectiveMonetizationType]
378
- || TRANSACTION_TYPE.INCOME;
379
-
380
- // Calculate commission if configured
381
- const commissionRate = this.config.commissionRates?.[category] || 0;
382
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] || 0;
383
- const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
384
-
385
- // Create transaction
386
- const TransactionModel = this.models.Transaction;
387
- const transaction = await TransactionModel.create({
388
- organizationId: subscription.organizationId,
389
- customerId: subscription.customerId,
390
- amount: subscription.amount,
391
- currency: subscription.currency || 'BDT',
392
- category,
393
- type: transactionType,
394
- method: paymentData?.method || 'manual',
395
- status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
396
- gateway: {
397
- type: gateway,
398
- paymentIntentId: paymentIntent.id,
399
- provider: paymentIntent.provider,
400
- },
401
- paymentDetails: {
402
- provider: gateway,
403
- ...paymentData,
404
- },
405
- ...(commission && { commission }), // Only include if commission exists
406
- // Polymorphic reference to subscription
407
- referenceId: subscription._id,
408
- referenceModel: 'Subscription',
409
- metadata: {
410
- ...metadata,
411
- subscriptionId: subscription._id.toString(), // Keep for backward compat
412
- entity: effectiveEntity,
413
- monetizationType: effectiveMonetizationType,
414
- isRenewal: true,
415
- paymentIntentId: paymentIntent.id,
416
- },
417
- idempotencyKey: idempotencyKey || `renewal_${nanoid(16)}`,
418
- });
419
-
420
- // Update subscription
421
- subscription.status = 'pending_renewal';
422
- subscription.renewalTransactionId = transaction._id;
423
- subscription.renewalCount = (subscription.renewalCount || 0) + 1;
424
- await subscription.save();
425
-
426
- // Trigger hook
427
- this._triggerHook('subscription.renewed', {
428
- subscription,
429
- transaction,
430
- paymentIntent,
431
- renewalCount: subscription.renewalCount,
432
- });
433
-
434
- return {
435
- subscription,
436
- transaction,
437
- paymentIntent,
438
- };
439
- }
440
-
441
- /**
442
- * Cancel subscription
443
- *
444
- * @param {String} subscriptionId - Subscription ID
445
- * @param {Object} options - Cancellation options
446
- * @param {Boolean} options.immediate - Cancel immediately vs at period end
447
- * @param {String} options.reason - Cancellation reason
448
- * @returns {Promise<Object>} Updated subscription
449
- */
450
- async cancel(subscriptionId, options = {}) {
451
- const { immediate = false, reason = null } = options;
452
-
453
- if (!this.models.Subscription) {
454
- throw new ModelNotRegisteredError('Subscription');
455
- }
456
-
457
- const SubscriptionModel = this.models.Subscription;
458
- const subscription = await SubscriptionModel.findById(subscriptionId);
459
-
460
- if (!subscription) {
461
- throw new SubscriptionNotFoundError(subscriptionId);
462
- }
463
-
464
- const now = new Date();
465
-
466
- if (immediate) {
467
- subscription.isActive = false;
468
- subscription.status = 'cancelled';
469
- subscription.canceledAt = now;
470
- subscription.cancellationReason = reason;
471
- } else {
472
- // Schedule cancellation at period end
473
- subscription.cancelAt = subscription.endDate || now;
474
- subscription.cancellationReason = reason;
475
- }
476
-
477
- await subscription.save();
478
-
479
- // Trigger hook
480
- this._triggerHook('subscription.cancelled', {
481
- subscription,
482
- immediate,
483
- reason,
484
- canceledAt: immediate ? now : subscription.cancelAt,
485
- });
486
-
487
- return subscription;
488
- }
489
-
490
- /**
491
- * Pause subscription
492
- *
493
- * @param {String} subscriptionId - Subscription ID
494
- * @param {Object} options - Pause options
495
- * @returns {Promise<Object>} Updated subscription
496
- */
497
- async pause(subscriptionId, options = {}) {
498
- const { reason = null } = options;
499
-
500
- if (!this.models.Subscription) {
501
- throw new ModelNotRegisteredError('Subscription');
502
- }
503
-
504
- const SubscriptionModel = this.models.Subscription;
505
- const subscription = await SubscriptionModel.findById(subscriptionId);
506
-
507
- if (!subscription) {
508
- throw new SubscriptionNotFoundError(subscriptionId);
509
- }
510
-
511
- if (!subscription.isActive) {
512
- throw new SubscriptionNotActiveError(subscriptionId, 'Only active subscriptions can be paused');
513
- }
514
-
515
- const pausedAt = new Date();
516
- subscription.isActive = false;
517
- subscription.status = 'paused';
518
- subscription.pausedAt = pausedAt;
519
- subscription.pauseReason = reason;
520
-
521
- await subscription.save();
522
-
523
- // Trigger hook
524
- this._triggerHook('subscription.paused', {
525
- subscription,
526
- reason,
527
- pausedAt,
528
- });
529
-
530
- return subscription;
531
- }
532
-
533
- /**
534
- * Resume subscription
535
- *
536
- * @param {String} subscriptionId - Subscription ID
537
- * @param {Object} options - Resume options
538
- * @returns {Promise<Object>} Updated subscription
539
- */
540
- async resume(subscriptionId, options = {}) {
541
- const { extendPeriod = false } = options;
542
-
543
- if (!this.models.Subscription) {
544
- throw new ModelNotRegisteredError('Subscription');
545
- }
546
-
547
- const SubscriptionModel = this.models.Subscription;
548
- const subscription = await SubscriptionModel.findById(subscriptionId);
549
-
550
- if (!subscription) {
551
- throw new SubscriptionNotFoundError(subscriptionId);
552
- }
553
-
554
- if (!subscription.pausedAt) {
555
- throw new InvalidStateTransitionError(
556
- 'resume',
557
- 'paused',
558
- subscription.status,
559
- 'Only paused subscriptions can be resumed'
560
- );
561
- }
562
-
563
- const now = new Date();
564
- const pausedAt = new Date(subscription.pausedAt);
565
- const pauseDuration = now - pausedAt;
566
-
567
- subscription.isActive = true;
568
- subscription.status = 'active';
569
- subscription.pausedAt = null;
570
- subscription.pauseReason = null;
571
-
572
- // Optionally extend period by pause duration
573
- if (extendPeriod && subscription.endDate) {
574
- const currentEnd = new Date(subscription.endDate);
575
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
576
- }
577
-
578
- await subscription.save();
579
-
580
- // Trigger hook
581
- this._triggerHook('subscription.resumed', {
582
- subscription,
583
- extendPeriod,
584
- pauseDuration,
585
- resumedAt: now,
586
- });
587
-
588
- return subscription;
589
- }
590
-
591
- /**
592
- * List subscriptions with filters
593
- *
594
- * @param {Object} filters - Query filters
595
- * @param {Object} options - Query options (limit, skip, sort)
596
- * @returns {Promise<Array>} Subscriptions
597
- */
598
- async list(filters = {}, options = {}) {
599
- if (!this.models.Subscription) {
600
- throw new ModelNotRegisteredError('Subscription');
601
- }
602
-
603
- const SubscriptionModel = this.models.Subscription;
604
- const { limit = 50, skip = 0, sort = { createdAt: -1 } } = options;
605
-
606
- const subscriptions = await SubscriptionModel
607
- .find(filters)
608
- .limit(limit)
609
- .skip(skip)
610
- .sort(sort);
611
-
612
- return subscriptions;
613
- }
614
-
615
- /**
616
- * Get subscription by ID
617
- *
618
- * @param {String} subscriptionId - Subscription ID
619
- * @returns {Promise<Object>} Subscription
620
- */
621
- async get(subscriptionId) {
622
- if (!this.models.Subscription) {
623
- throw new ModelNotRegisteredError('Subscription');
624
- }
625
-
626
- const SubscriptionModel = this.models.Subscription;
627
- const subscription = await SubscriptionModel.findById(subscriptionId);
628
-
629
- if (!subscription) {
630
- throw new SubscriptionNotFoundError(subscriptionId);
631
- }
632
-
633
- return subscription;
634
- }
635
-
636
- /**
637
- * Calculate period end date based on plan key
638
- * @private
639
- */
640
- _calculatePeriodEnd(planKey, startDate = new Date()) {
641
- const start = new Date(startDate);
642
- let end = new Date(start);
643
-
644
- switch (planKey) {
645
- case 'monthly':
646
- end.setMonth(end.getMonth() + 1);
647
- break;
648
- case 'quarterly':
649
- end.setMonth(end.getMonth() + 3);
650
- break;
651
- case 'yearly':
652
- end.setFullYear(end.getFullYear() + 1);
653
- break;
654
- default:
655
- // Default to 30 days
656
- end.setDate(end.getDate() + 30);
657
- }
658
-
659
- return end;
660
- }
661
-
662
- /**
663
- * Trigger event hook (fire-and-forget, non-blocking)
664
- * @private
665
- */
666
- _triggerHook(event, data) {
667
- triggerHook(this.hooks, event, data, this.logger);
668
- }
669
- }
670
-
671
- export default MonetizationService;