@classytic/revenue 0.0.24 → 0.2.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 (57) hide show
  1. package/README.md +184 -24
  2. package/core/builder.js +51 -4
  3. package/dist/types/core/builder.d.ts +95 -0
  4. package/dist/types/core/container.d.ts +57 -0
  5. package/dist/types/core/errors.d.ts +122 -0
  6. package/dist/types/enums/escrow.enums.d.ts +24 -0
  7. package/dist/types/enums/index.d.ts +69 -0
  8. package/dist/types/enums/monetization.enums.d.ts +6 -0
  9. package/dist/types/enums/payment.enums.d.ts +16 -0
  10. package/dist/types/enums/split.enums.d.ts +25 -0
  11. package/dist/types/enums/subscription.enums.d.ts +15 -0
  12. package/dist/types/enums/transaction.enums.d.ts +24 -0
  13. package/dist/types/index.d.ts +22 -0
  14. package/dist/types/providers/base.d.ts +126 -0
  15. package/dist/types/schemas/escrow/hold.schema.d.ts +54 -0
  16. package/dist/types/schemas/escrow/index.d.ts +6 -0
  17. package/dist/types/schemas/index.d.ts +506 -0
  18. package/dist/types/schemas/split/index.d.ts +8 -0
  19. package/dist/types/schemas/split/split.schema.d.ts +142 -0
  20. package/dist/types/schemas/subscription/index.d.ts +152 -0
  21. package/dist/types/schemas/subscription/info.schema.d.ts +128 -0
  22. package/dist/types/schemas/subscription/plan.schema.d.ts +39 -0
  23. package/dist/types/schemas/transaction/common.schema.d.ts +12 -0
  24. package/dist/types/schemas/transaction/gateway.schema.d.ts +86 -0
  25. package/dist/types/schemas/transaction/index.d.ts +202 -0
  26. package/dist/types/schemas/transaction/payment.schema.d.ts +145 -0
  27. package/dist/types/services/escrow.service.d.ts +51 -0
  28. package/dist/types/services/payment.service.d.ts +80 -0
  29. package/dist/types/services/subscription.service.d.ts +139 -0
  30. package/dist/types/services/transaction.service.d.ts +40 -0
  31. package/dist/types/utils/category-resolver.d.ts +46 -0
  32. package/dist/types/utils/commission-split.d.ts +56 -0
  33. package/dist/types/utils/commission.d.ts +29 -0
  34. package/dist/types/utils/hooks.d.ts +17 -0
  35. package/dist/types/utils/index.d.ts +6 -0
  36. package/dist/types/utils/logger.d.ts +12 -0
  37. package/dist/types/utils/subscription/actions.d.ts +28 -0
  38. package/dist/types/utils/subscription/index.d.ts +2 -0
  39. package/dist/types/utils/subscription/period.d.ts +47 -0
  40. package/dist/types/utils/transaction-type.d.ts +102 -0
  41. package/enums/escrow.enums.js +36 -0
  42. package/enums/index.js +36 -0
  43. package/enums/payment.enums.js +26 -5
  44. package/enums/split.enums.js +37 -0
  45. package/index.js +8 -2
  46. package/package.json +91 -74
  47. package/schemas/escrow/hold.schema.js +62 -0
  48. package/schemas/escrow/index.js +15 -0
  49. package/schemas/index.js +6 -0
  50. package/schemas/split/index.js +16 -0
  51. package/schemas/split/split.schema.js +86 -0
  52. package/services/escrow.service.js +353 -0
  53. package/services/payment.service.js +64 -3
  54. package/services/subscription.service.js +36 -16
  55. package/utils/commission-split.js +180 -0
  56. package/utils/index.js +6 -0
  57. package/revenue.d.ts +0 -350
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Split Payment Schema
3
+ * @classytic/revenue
4
+ *
5
+ * Schema for multi-party commission splits
6
+ * Spread into transaction schema when needed
7
+ */
8
+
9
+ import { SPLIT_TYPE, SPLIT_TYPE_VALUES, SPLIT_STATUS, SPLIT_STATUS_VALUES, PAYOUT_METHOD, PAYOUT_METHOD_VALUES } from '../../enums/split.enums.js';
10
+
11
+ export const splitItemSchema = {
12
+ type: {
13
+ type: String,
14
+ enum: SPLIT_TYPE_VALUES,
15
+ required: true,
16
+ },
17
+
18
+ recipientId: {
19
+ type: String,
20
+ required: true,
21
+ index: true,
22
+ },
23
+
24
+ recipientType: {
25
+ type: String,
26
+ required: true,
27
+ },
28
+
29
+ rate: {
30
+ type: Number,
31
+ required: true,
32
+ min: 0,
33
+ max: 1,
34
+ },
35
+
36
+ grossAmount: {
37
+ type: Number,
38
+ required: true,
39
+ },
40
+
41
+ gatewayFeeRate: {
42
+ type: Number,
43
+ default: 0,
44
+ },
45
+
46
+ gatewayFeeAmount: {
47
+ type: Number,
48
+ default: 0,
49
+ },
50
+
51
+ netAmount: {
52
+ type: Number,
53
+ required: true,
54
+ },
55
+
56
+ status: {
57
+ type: String,
58
+ enum: SPLIT_STATUS_VALUES,
59
+ default: SPLIT_STATUS.PENDING,
60
+ },
61
+
62
+ dueDate: Date,
63
+ paidDate: Date,
64
+
65
+ payoutMethod: {
66
+ type: String,
67
+ enum: PAYOUT_METHOD_VALUES,
68
+ required: false,
69
+ },
70
+
71
+ payoutTransactionId: String,
72
+
73
+ metadata: {
74
+ type: Object,
75
+ default: {},
76
+ },
77
+ };
78
+
79
+ export const splitsSchema = {
80
+ splits: {
81
+ type: [splitItemSchema],
82
+ default: [],
83
+ },
84
+ };
85
+
86
+ export default splitsSchema;
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Escrow Service
3
+ * @classytic/revenue
4
+ *
5
+ * Platform-as-intermediary payment flow
6
+ * Hold funds → Verify → Split/Deduct → Release to organization
7
+ */
8
+
9
+ import { TransactionNotFoundError } from '../core/errors.js';
10
+ import { HOLD_STATUS, RELEASE_REASON, HOLD_REASON } from '../enums/escrow.enums.js';
11
+ import { TRANSACTION_TYPE, TRANSACTION_STATUS } from '../enums/transaction.enums.js';
12
+ import { SPLIT_STATUS } from '../enums/split.enums.js';
13
+ import { triggerHook } from '../utils/hooks.js';
14
+ import { calculateSplits, calculateOrganizationPayout } from '../utils/commission-split.js';
15
+
16
+ export class EscrowService {
17
+ constructor(container) {
18
+ this.container = container;
19
+ this.models = container.get('models');
20
+ this.providers = container.get('providers');
21
+ this.config = container.get('config');
22
+ this.hooks = container.get('hooks');
23
+ this.logger = container.get('logger');
24
+ }
25
+
26
+ /**
27
+ * Hold funds in escrow
28
+ *
29
+ * @param {String} transactionId - Transaction to hold
30
+ * @param {Object} options - Hold options
31
+ * @returns {Promise<Object>} Updated transaction
32
+ */
33
+ async hold(transactionId, options = {}) {
34
+ const {
35
+ reason = HOLD_REASON.PAYMENT_VERIFICATION,
36
+ holdUntil = null,
37
+ metadata = {},
38
+ } = options;
39
+
40
+ const TransactionModel = this.models.Transaction;
41
+ const transaction = await TransactionModel.findById(transactionId);
42
+
43
+ if (!transaction) {
44
+ throw new TransactionNotFoundError(transactionId);
45
+ }
46
+
47
+ if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
48
+ throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
49
+ }
50
+
51
+ transaction.hold = {
52
+ status: HOLD_STATUS.HELD,
53
+ heldAmount: transaction.amount,
54
+ releasedAmount: 0,
55
+ reason,
56
+ heldAt: new Date(),
57
+ ...(holdUntil && { holdUntil }),
58
+ releases: [],
59
+ metadata,
60
+ };
61
+
62
+ await transaction.save();
63
+
64
+ this._triggerHook('escrow.held', {
65
+ transaction,
66
+ heldAmount: transaction.amount,
67
+ reason,
68
+ });
69
+
70
+ return transaction;
71
+ }
72
+
73
+ /**
74
+ * Release funds from escrow to recipient
75
+ *
76
+ * @param {String} transactionId - Transaction to release
77
+ * @param {Object} options - Release options
78
+ * @returns {Promise<Object>} { transaction, releaseTransaction }
79
+ */
80
+ async release(transactionId, options = {}) {
81
+ const {
82
+ amount = null,
83
+ recipientId,
84
+ recipientType = 'organization',
85
+ reason = RELEASE_REASON.PAYMENT_VERIFIED,
86
+ releasedBy = null,
87
+ createTransaction = true,
88
+ metadata = {},
89
+ } = options;
90
+
91
+ const TransactionModel = this.models.Transaction;
92
+ const transaction = await TransactionModel.findById(transactionId);
93
+
94
+ if (!transaction) {
95
+ throw new TransactionNotFoundError(transactionId);
96
+ }
97
+
98
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
99
+ throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status || 'none'}`);
100
+ }
101
+
102
+ if (!recipientId) {
103
+ throw new Error('recipientId is required for release');
104
+ }
105
+
106
+ const releaseAmount = amount || (transaction.hold.heldAmount - transaction.hold.releasedAmount);
107
+ const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
108
+
109
+ if (releaseAmount > availableAmount) {
110
+ throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
111
+ }
112
+
113
+ const releaseRecord = {
114
+ amount: releaseAmount,
115
+ recipientId,
116
+ recipientType,
117
+ releasedAt: new Date(),
118
+ releasedBy,
119
+ reason,
120
+ metadata,
121
+ };
122
+
123
+ transaction.hold.releases.push(releaseRecord);
124
+ transaction.hold.releasedAmount += releaseAmount;
125
+
126
+ const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
127
+ const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
128
+
129
+ if (isFullRelease) {
130
+ transaction.hold.status = HOLD_STATUS.RELEASED;
131
+ transaction.hold.releasedAt = new Date();
132
+ transaction.status = TRANSACTION_STATUS.COMPLETED;
133
+ } else if (isPartialRelease) {
134
+ transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
135
+ }
136
+
137
+ await transaction.save();
138
+
139
+ let releaseTransaction = null;
140
+ if (createTransaction) {
141
+ releaseTransaction = await TransactionModel.create({
142
+ organizationId: transaction.organizationId,
143
+ customerId: recipientId,
144
+ amount: releaseAmount,
145
+ currency: transaction.currency,
146
+ category: transaction.category,
147
+ type: TRANSACTION_TYPE.INCOME,
148
+ method: transaction.method,
149
+ status: TRANSACTION_STATUS.COMPLETED,
150
+ gateway: transaction.gateway,
151
+ referenceId: transaction.referenceId,
152
+ referenceModel: transaction.referenceModel,
153
+ metadata: {
154
+ ...metadata,
155
+ isRelease: true,
156
+ heldTransactionId: transaction._id.toString(),
157
+ releaseReason: reason,
158
+ recipientType,
159
+ },
160
+ idempotencyKey: `release_${transaction._id}_${Date.now()}`,
161
+ });
162
+ }
163
+
164
+ this._triggerHook('escrow.released', {
165
+ transaction,
166
+ releaseTransaction,
167
+ releaseAmount,
168
+ recipientId,
169
+ recipientType,
170
+ reason,
171
+ isFullRelease,
172
+ isPartialRelease,
173
+ });
174
+
175
+ return {
176
+ transaction,
177
+ releaseTransaction,
178
+ releaseAmount,
179
+ isFullRelease,
180
+ isPartialRelease,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Cancel hold and release back to customer
186
+ *
187
+ * @param {String} transactionId - Transaction to cancel hold
188
+ * @param {Object} options - Cancel options
189
+ * @returns {Promise<Object>} Updated transaction
190
+ */
191
+ async cancel(transactionId, options = {}) {
192
+ const { reason = 'Hold cancelled', metadata = {} } = options;
193
+
194
+ const TransactionModel = this.models.Transaction;
195
+ const transaction = await TransactionModel.findById(transactionId);
196
+
197
+ if (!transaction) {
198
+ throw new TransactionNotFoundError(transactionId);
199
+ }
200
+
201
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
202
+ throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status || 'none'}`);
203
+ }
204
+
205
+ transaction.hold.status = HOLD_STATUS.CANCELLED;
206
+ transaction.hold.cancelledAt = new Date();
207
+ transaction.hold.metadata = {
208
+ ...transaction.hold.metadata,
209
+ ...metadata,
210
+ cancelReason: reason,
211
+ };
212
+
213
+ transaction.status = TRANSACTION_STATUS.CANCELLED;
214
+
215
+ await transaction.save();
216
+
217
+ this._triggerHook('escrow.cancelled', {
218
+ transaction,
219
+ reason,
220
+ });
221
+
222
+ return transaction;
223
+ }
224
+
225
+ /**
226
+ * Split payment to multiple recipients
227
+ * Deducts splits from held amount and releases remainder to organization
228
+ *
229
+ * @param {String} transactionId - Transaction to split
230
+ * @param {Array} splitRules - Split configuration
231
+ * @returns {Promise<Object>} { transaction, splitTransactions, organizationTransaction }
232
+ */
233
+ async split(transactionId, splitRules = []) {
234
+ const TransactionModel = this.models.Transaction;
235
+ const transaction = await TransactionModel.findById(transactionId);
236
+
237
+ if (!transaction) {
238
+ throw new TransactionNotFoundError(transactionId);
239
+ }
240
+
241
+ if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
242
+ throw new Error(`Transaction must be held before splitting. Current: ${transaction.hold?.status || 'none'}`);
243
+ }
244
+
245
+ if (!splitRules || splitRules.length === 0) {
246
+ throw new Error('splitRules cannot be empty');
247
+ }
248
+
249
+ const splits = calculateSplits(
250
+ transaction.amount,
251
+ splitRules,
252
+ transaction.commission?.gatewayFeeRate || 0
253
+ );
254
+
255
+ transaction.splits = splits;
256
+ await transaction.save();
257
+
258
+ const splitTransactions = [];
259
+
260
+ for (const split of splits) {
261
+ const splitTransaction = await TransactionModel.create({
262
+ organizationId: transaction.organizationId,
263
+ customerId: split.recipientId,
264
+ amount: split.netAmount,
265
+ currency: transaction.currency,
266
+ category: split.type,
267
+ type: TRANSACTION_TYPE.EXPENSE,
268
+ method: transaction.method,
269
+ status: TRANSACTION_STATUS.COMPLETED,
270
+ gateway: transaction.gateway,
271
+ referenceId: transaction.referenceId,
272
+ referenceModel: transaction.referenceModel,
273
+ metadata: {
274
+ isSplit: true,
275
+ splitType: split.type,
276
+ recipientType: split.recipientType,
277
+ originalTransactionId: transaction._id.toString(),
278
+ grossAmount: split.grossAmount,
279
+ gatewayFeeAmount: split.gatewayFeeAmount,
280
+ },
281
+ idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`,
282
+ });
283
+
284
+ split.payoutTransactionId = splitTransaction._id.toString();
285
+ split.status = SPLIT_STATUS.PAID;
286
+ split.paidDate = new Date();
287
+
288
+ splitTransactions.push(splitTransaction);
289
+ }
290
+
291
+ await transaction.save();
292
+
293
+ const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
294
+
295
+ const organizationTransaction = await this.release(transactionId, {
296
+ amount: organizationPayout,
297
+ recipientId: transaction.organizationId,
298
+ recipientType: 'organization',
299
+ reason: RELEASE_REASON.PAYMENT_VERIFIED,
300
+ createTransaction: true,
301
+ metadata: {
302
+ afterSplits: true,
303
+ totalSplits: splits.length,
304
+ totalSplitAmount: transaction.amount - organizationPayout,
305
+ },
306
+ });
307
+
308
+ this._triggerHook('escrow.split', {
309
+ transaction,
310
+ splits,
311
+ splitTransactions,
312
+ organizationTransaction: organizationTransaction.releaseTransaction,
313
+ organizationPayout,
314
+ });
315
+
316
+ return {
317
+ transaction,
318
+ splits,
319
+ splitTransactions,
320
+ organizationTransaction: organizationTransaction.releaseTransaction,
321
+ organizationPayout,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Get escrow status
327
+ *
328
+ * @param {String} transactionId - Transaction ID
329
+ * @returns {Promise<Object>} Escrow status
330
+ */
331
+ async getStatus(transactionId) {
332
+ const TransactionModel = this.models.Transaction;
333
+ const transaction = await TransactionModel.findById(transactionId);
334
+
335
+ if (!transaction) {
336
+ throw new TransactionNotFoundError(transactionId);
337
+ }
338
+
339
+ return {
340
+ transaction,
341
+ hold: transaction.hold || null,
342
+ splits: transaction.splits || [],
343
+ hasHold: !!transaction.hold,
344
+ hasSplits: transaction.splits && transaction.splits.length > 0,
345
+ };
346
+ }
347
+
348
+ _triggerHook(event, data) {
349
+ triggerHook(this.hooks, event, data, this.logger);
350
+ }
351
+ }
352
+
353
+ export default EscrowService;
@@ -9,11 +9,13 @@
9
9
  import {
10
10
  TransactionNotFoundError,
11
11
  ProviderNotFoundError,
12
+ ProviderError,
12
13
  AlreadyVerifiedError,
13
14
  PaymentVerificationError,
14
15
  RefundNotSupportedError,
15
16
  RefundError,
16
17
  ProviderCapabilityError,
18
+ ValidationError,
17
19
  } from '../core/errors.js';
18
20
  import { triggerHook } from '../utils/hooks.js';
19
21
  import { reverseCommission } from '../utils/commission.js';
@@ -81,15 +83,40 @@ export class PaymentService {
81
83
 
82
84
  // Update transaction as failed
83
85
  transaction.status = 'failed';
86
+ transaction.failureReason = error.message;
84
87
  transaction.metadata = {
85
88
  ...transaction.metadata,
86
89
  verificationError: error.message,
90
+ failedAt: new Date().toISOString(),
87
91
  };
88
92
  await transaction.save();
89
93
 
94
+ // Trigger payment.failed hook
95
+ this._triggerHook('payment.failed', {
96
+ transaction,
97
+ error: error.message,
98
+ provider: gatewayType,
99
+ paymentIntentId,
100
+ });
101
+
90
102
  throw new PaymentVerificationError(paymentIntentId, error.message);
91
103
  }
92
104
 
105
+ // Validate amount and currency match
106
+ if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
107
+ throw new ValidationError(
108
+ `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
109
+ { expected: transaction.amount, actual: paymentResult.amount }
110
+ );
111
+ }
112
+
113
+ if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
114
+ throw new ValidationError(
115
+ `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
116
+ { expected: transaction.currency, actual: paymentResult.currency }
117
+ );
118
+ }
119
+
93
120
  // Update transaction based on verification result
94
121
  transaction.status = paymentResult.status === 'succeeded' ? 'verified' : paymentResult.status;
95
122
  transaction.verifiedAt = paymentResult.paidAt || new Date();
@@ -111,7 +138,7 @@ export class PaymentService {
111
138
  return {
112
139
  transaction,
113
140
  paymentResult,
114
- status: 'verified',
141
+ status: transaction.status,
115
142
  };
116
143
  }
117
144
 
@@ -212,8 +239,24 @@ export class PaymentService {
212
239
  throw new RefundNotSupportedError(gatewayType);
213
240
  }
214
241
 
242
+ // Calculate refundable amount
243
+ const refundedSoFar = transaction.refundedAmount || 0;
244
+ const refundableAmount = transaction.amount - refundedSoFar;
245
+ const refundAmount = amount || refundableAmount;
246
+
247
+ // Validate refund amount
248
+ if (refundAmount <= 0) {
249
+ throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
250
+ }
251
+
252
+ if (refundAmount > refundableAmount) {
253
+ throw new ValidationError(
254
+ `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
255
+ { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
256
+ );
257
+ }
258
+
215
259
  // Refund via provider
216
- const refundAmount = amount || transaction.amount;
217
260
  let refundResult = null;
218
261
 
219
262
  try {
@@ -306,13 +349,31 @@ export class PaymentService {
306
349
  throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
307
350
  }
308
351
 
352
+ // Check if provider supports webhooks
353
+ const capabilities = provider.getCapabilities();
354
+ if (!capabilities.supportsWebhooks) {
355
+ throw new ProviderCapabilityError(providerName, 'webhooks');
356
+ }
357
+
309
358
  // Process webhook via provider
310
359
  let webhookEvent = null;
311
360
  try {
312
361
  webhookEvent = await provider.handleWebhook(payload, headers);
313
362
  } catch (error) {
314
363
  this.logger.error('Webhook processing failed:', error);
315
- throw new ProviderError(providerName, `Webhook processing failed: ${error.message}`);
364
+ throw new ProviderError(
365
+ `Webhook processing failed for ${providerName}: ${error.message}`,
366
+ 'WEBHOOK_PROCESSING_FAILED',
367
+ { retryable: false }
368
+ );
369
+ }
370
+
371
+ // Validate webhook event structure
372
+ if (!webhookEvent?.data?.paymentIntentId) {
373
+ throw new ValidationError(
374
+ `Invalid webhook event structure from ${providerName}: missing paymentIntentId`,
375
+ { provider: providerName, eventType: webhookEvent?.type }
376
+ );
316
377
  }
317
378
 
318
379
  // Find transaction by payment intent ID from webhook
@@ -45,14 +45,14 @@ export class SubscriptionService {
45
45
  * @param {String} params.planKey - Plan key ('monthly', 'quarterly', 'yearly')
46
46
  * @param {Number} params.amount - Subscription amount
47
47
  * @param {String} params.currency - Currency code (default: 'BDT')
48
- * @param {String} params.gateway - Payment gateway to use (default: 'manual')
48
+ * @param {String} params.gateway - Payment gateway name (default: 'manual') - Use ANY registered provider name: 'manual', 'bkash', 'nagad', 'stripe', etc.
49
49
  * @param {String} params.entity - Logical entity identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
50
50
  * NOTE: This is NOT a database model name - it's just a logical identifier for categoryMappings
51
51
  * @param {String} params.monetizationType - Monetization type ('free', 'subscription', 'purchase')
52
52
  * @param {Object} params.paymentData - Payment method details
53
53
  * @param {Object} params.metadata - Additional metadata
54
54
  * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
55
- *
55
+ *
56
56
  * @example
57
57
  * // With polymorphic reference (recommended)
58
58
  * await revenue.subscriptions.create({
@@ -62,10 +62,11 @@ export class SubscriptionService {
62
62
  * referenceId: subscription._id, // Links to entity
63
63
  * referenceModel: 'Subscription', // Model name
64
64
  * },
65
+ * gateway: 'bkash', // Any registered provider
65
66
  * amount: 1500,
66
67
  * // ...
67
68
  * });
68
- *
69
+ *
69
70
  * @returns {Promise<Object>} { subscription, transaction, paymentIntent }
70
71
  */
71
72
  async create(params) {
@@ -204,13 +205,26 @@ export class SubscriptionService {
204
205
  subscription = await SubscriptionModel.create(subscriptionData);
205
206
  }
206
207
 
207
- // Trigger hook
208
- this._triggerHook('subscription.created', {
208
+ // Trigger hooks - emit specific event based on monetization type
209
+ const eventData = {
209
210
  subscription,
210
211
  transaction,
211
212
  paymentIntent,
212
213
  isFree,
213
- });
214
+ monetizationType,
215
+ };
216
+
217
+ // Emit specific monetization event
218
+ if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
219
+ this._triggerHook('purchase.created', eventData);
220
+ } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
221
+ this._triggerHook('subscription.created', eventData);
222
+ } else if (monetizationType === MONETIZATION_TYPES.FREE) {
223
+ this._triggerHook('free.created', eventData);
224
+ }
225
+
226
+ // Also emit generic event for backward compatibility
227
+ this._triggerHook('monetization.created', eventData);
214
228
 
215
229
  return {
216
230
  subscription,
@@ -271,7 +285,7 @@ export class SubscriptionService {
271
285
  *
272
286
  * @param {String} subscriptionId - Subscription ID
273
287
  * @param {Object} params - Renewal parameters
274
- * @param {String} params.gateway - Payment gateway to use (default: 'manual')
288
+ * @param {String} params.gateway - Payment gateway name (default: 'manual') - Use ANY registered provider name: 'manual', 'bkash', 'nagad', 'stripe', etc.
275
289
  * @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
276
290
  * @param {Object} params.paymentData - Payment method details
277
291
  * @param {Object} params.metadata - Additional metadata
@@ -309,15 +323,21 @@ export class SubscriptionService {
309
323
  }
310
324
 
311
325
  // Create payment intent
312
- const paymentIntent = await provider.createIntent({
313
- amount: subscription.amount,
314
- currency: subscription.currency || 'BDT',
315
- metadata: {
316
- ...metadata,
317
- type: 'subscription_renewal',
318
- subscriptionId: subscription._id.toString(),
319
- },
320
- });
326
+ let paymentIntent = null;
327
+ try {
328
+ paymentIntent = await provider.createIntent({
329
+ amount: subscription.amount,
330
+ currency: subscription.currency || 'BDT',
331
+ metadata: {
332
+ ...metadata,
333
+ type: 'subscription_renewal',
334
+ subscriptionId: subscription._id.toString(),
335
+ },
336
+ });
337
+ } catch (error) {
338
+ this.logger.error('Failed to create payment intent for renewal:', error);
339
+ throw new PaymentIntentCreationError(gateway, error);
340
+ }
321
341
 
322
342
  // Resolve category - use provided entity or inherit from subscription metadata
323
343
  const effectiveEntity = entity || subscription.metadata?.entity;