@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.
- package/README.md +498 -499
- package/dist/actions-CwG-b7fR.d.ts +519 -0
- package/dist/core/index.d.ts +884 -0
- package/dist/core/index.js +2941 -0
- package/dist/core/index.js.map +1 -0
- package/dist/enums/index.d.ts +130 -0
- package/dist/enums/index.js +167 -0
- package/dist/enums/index.js.map +1 -0
- package/dist/index-BnJWVXuw.d.ts +378 -0
- package/dist/index-ChVD3P9k.d.ts +504 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +4353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +132 -0
- package/dist/providers/index.js +122 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/retry-80lBCmSe.d.ts +234 -0
- package/dist/schemas/index.d.ts +894 -0
- package/dist/schemas/index.js +524 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/validation.d.ts +309 -0
- package/dist/schemas/validation.js +249 -0
- package/dist/schemas/validation.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1632 -0
- package/dist/services/index.js.map +1 -0
- package/dist/split.enums-DHdM1YAV.d.ts +255 -0
- package/dist/split.schema-BPdFZMbU.d.ts +958 -0
- package/dist/utils/index.d.ts +24 -0
- package/dist/utils/index.js +1067 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +48 -32
- package/core/builder.js +0 -219
- package/core/container.js +0 -119
- package/core/errors.js +0 -262
- package/dist/types/core/builder.d.ts +0 -97
- package/dist/types/core/container.d.ts +0 -57
- package/dist/types/core/errors.d.ts +0 -122
- package/dist/types/enums/escrow.enums.d.ts +0 -24
- package/dist/types/enums/index.d.ts +0 -69
- package/dist/types/enums/monetization.enums.d.ts +0 -6
- package/dist/types/enums/payment.enums.d.ts +0 -16
- package/dist/types/enums/split.enums.d.ts +0 -25
- package/dist/types/enums/subscription.enums.d.ts +0 -15
- package/dist/types/enums/transaction.enums.d.ts +0 -24
- package/dist/types/index.d.ts +0 -22
- package/dist/types/providers/base.d.ts +0 -126
- package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
- package/dist/types/schemas/escrow/index.d.ts +0 -6
- package/dist/types/schemas/index.d.ts +0 -506
- package/dist/types/schemas/split/index.d.ts +0 -8
- package/dist/types/schemas/split/split.schema.d.ts +0 -142
- package/dist/types/schemas/subscription/index.d.ts +0 -152
- package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
- package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
- package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
- package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
- package/dist/types/schemas/transaction/index.d.ts +0 -202
- package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
- package/dist/types/services/escrow.service.d.ts +0 -51
- package/dist/types/services/monetization.service.d.ts +0 -193
- package/dist/types/services/payment.service.d.ts +0 -112
- package/dist/types/services/transaction.service.d.ts +0 -40
- package/dist/types/utils/category-resolver.d.ts +0 -46
- package/dist/types/utils/commission-split.d.ts +0 -56
- package/dist/types/utils/commission.d.ts +0 -29
- package/dist/types/utils/hooks.d.ts +0 -17
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/logger.d.ts +0 -12
- package/dist/types/utils/subscription/actions.d.ts +0 -28
- package/dist/types/utils/subscription/index.d.ts +0 -2
- package/dist/types/utils/subscription/period.d.ts +0 -47
- package/dist/types/utils/transaction-type.d.ts +0 -102
- package/enums/escrow.enums.js +0 -36
- package/enums/index.d.ts +0 -116
- package/enums/index.js +0 -110
- package/enums/monetization.enums.js +0 -15
- package/enums/payment.enums.js +0 -64
- package/enums/split.enums.js +0 -37
- package/enums/subscription.enums.js +0 -33
- package/enums/transaction.enums.js +0 -69
- package/index.js +0 -76
- package/providers/base.js +0 -162
- package/schemas/escrow/hold.schema.js +0 -62
- package/schemas/escrow/index.js +0 -15
- package/schemas/index.d.ts +0 -33
- package/schemas/index.js +0 -27
- package/schemas/split/index.js +0 -16
- package/schemas/split/split.schema.js +0 -86
- package/schemas/subscription/index.js +0 -17
- package/schemas/subscription/info.schema.js +0 -115
- package/schemas/subscription/plan.schema.js +0 -48
- package/schemas/transaction/common.schema.js +0 -22
- package/schemas/transaction/gateway.schema.js +0 -69
- package/schemas/transaction/index.js +0 -20
- package/schemas/transaction/payment.schema.js +0 -110
- package/services/escrow.service.js +0 -353
- package/services/monetization.service.js +0 -671
- package/services/payment.service.js +0 -517
- package/services/transaction.service.js +0 -142
- package/utils/category-resolver.js +0 -74
- package/utils/commission-split.js +0 -180
- package/utils/commission.js +0 -83
- package/utils/hooks.js +0 -44
- package/utils/index.d.ts +0 -164
- package/utils/index.js +0 -16
- package/utils/logger.js +0 -36
- package/utils/subscription/actions.js +0 -68
- package/utils/subscription/index.js +0 -20
- package/utils/subscription/period.js +0 -123
- 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;
|