@classytic/revenue 0.0.21 → 0.0.23

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.
@@ -15,10 +15,13 @@ import {
15
15
  ModelNotRegisteredError,
16
16
  SubscriptionNotActiveError,
17
17
  PaymentIntentCreationError,
18
+ InvalidStateTransitionError,
18
19
  } from '../core/errors.js';
19
20
  import { triggerHook } from '../utils/hooks.js';
20
21
  import { resolveCategory } from '../utils/category-resolver.js';
22
+ import { calculateCommission } from '../utils/commission.js';
21
23
  import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
24
+ import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
22
25
 
23
26
  /**
24
27
  * Subscription Service
@@ -38,7 +41,7 @@ export class SubscriptionService {
38
41
  * Create a new subscription
39
42
  *
40
43
  * @param {Object} params - Subscription parameters
41
- * @param {Object} params.data - Subscription data (organizationId, customerId, etc.)
44
+ * @param {Object} params.data - Subscription data (organizationId, customerId, referenceId, referenceModel, etc.)
42
45
  * @param {String} params.planKey - Plan key ('monthly', 'quarterly', 'yearly')
43
46
  * @param {Number} params.amount - Subscription amount
44
47
  * @param {String} params.currency - Currency code (default: 'BDT')
@@ -49,6 +52,20 @@ export class SubscriptionService {
49
52
  * @param {Object} params.paymentData - Payment method details
50
53
  * @param {Object} params.metadata - Additional metadata
51
54
  * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
55
+ *
56
+ * @example
57
+ * // With polymorphic reference (recommended)
58
+ * await revenue.subscriptions.create({
59
+ * data: {
60
+ * organizationId: '...',
61
+ * customerId: '...',
62
+ * referenceId: subscription._id, // Links to entity
63
+ * referenceModel: 'Subscription', // Model name
64
+ * },
65
+ * amount: 1500,
66
+ * // ...
67
+ * });
68
+ *
52
69
  * @returns {Promise<Object>} { subscription, transaction, paymentIntent }
53
70
  */
54
71
  async create(params) {
@@ -109,6 +126,16 @@ export class SubscriptionService {
109
126
  // Resolve category based on entity and monetizationType
110
127
  const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
111
128
 
129
+ // Resolve transaction type using config mapping or default to 'income'
130
+ const transactionType = this.config.transactionTypeMapping?.subscription
131
+ || this.config.transactionTypeMapping?.[monetizationType]
132
+ || TRANSACTION_TYPE.INCOME;
133
+
134
+ // Calculate commission if configured
135
+ const commissionRate = this.config.commissionRates?.[category] || 0;
136
+ const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] || 0;
137
+ const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
138
+
112
139
  // Create transaction record
113
140
  const TransactionModel = this.models.Transaction;
114
141
  transaction = await TransactionModel.create({
@@ -117,7 +144,8 @@ export class SubscriptionService {
117
144
  amount,
118
145
  currency,
119
146
  category,
120
- type: 'credit',
147
+ type: transactionType,
148
+ method: paymentData?.method || 'manual',
121
149
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
122
150
  gateway: {
123
151
  type: gateway,
@@ -128,6 +156,10 @@ export class SubscriptionService {
128
156
  provider: gateway,
129
157
  ...paymentData,
130
158
  },
159
+ ...(commission && { commission }), // Only include if commission exists
160
+ // Polymorphic reference (top-level, not metadata)
161
+ ...(data.referenceId && { referenceId: data.referenceId }),
162
+ ...(data.referenceModel && { referenceModel: data.referenceModel }),
131
163
  metadata: {
132
164
  ...metadata,
133
165
  planKey,
@@ -144,7 +176,8 @@ export class SubscriptionService {
144
176
  if (this.models.Subscription) {
145
177
  const SubscriptionModel = this.models.Subscription;
146
178
 
147
- subscription = await SubscriptionModel.create({
179
+ // Create subscription with proper reference tracking
180
+ const subscriptionData = {
148
181
  organizationId: data.organizationId,
149
182
  customerId: data.customerId || null,
150
183
  planKey,
@@ -162,7 +195,13 @@ export class SubscriptionService {
162
195
  monetizationType,
163
196
  },
164
197
  ...data,
165
- });
198
+ };
199
+
200
+ // Remove referenceId/referenceModel from subscription (they're for transactions)
201
+ delete subscriptionData.referenceId;
202
+ delete subscriptionData.referenceModel;
203
+
204
+ subscription = await SubscriptionModel.create(subscriptionData);
166
205
  }
167
206
 
168
207
  // Trigger hook
@@ -285,6 +324,17 @@ export class SubscriptionService {
285
324
  const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
286
325
  const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
287
326
 
327
+ // Resolve transaction type using config mapping or default to 'income'
328
+ const transactionType = this.config.transactionTypeMapping?.subscription_renewal
329
+ || this.config.transactionTypeMapping?.subscription
330
+ || this.config.transactionTypeMapping?.[effectiveMonetizationType]
331
+ || TRANSACTION_TYPE.INCOME;
332
+
333
+ // Calculate commission if configured
334
+ const commissionRate = this.config.commissionRates?.[category] || 0;
335
+ const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] || 0;
336
+ const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
337
+
288
338
  // Create transaction
289
339
  const TransactionModel = this.models.Transaction;
290
340
  const transaction = await TransactionModel.create({
@@ -293,7 +343,8 @@ export class SubscriptionService {
293
343
  amount: subscription.amount,
294
344
  currency: subscription.currency || 'BDT',
295
345
  category,
296
- type: 'credit',
346
+ type: transactionType,
347
+ method: paymentData?.method || 'manual',
297
348
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
298
349
  gateway: {
299
350
  type: gateway,
@@ -304,9 +355,13 @@ export class SubscriptionService {
304
355
  provider: gateway,
305
356
  ...paymentData,
306
357
  },
358
+ ...(commission && { commission }), // Only include if commission exists
359
+ // Polymorphic reference to subscription
360
+ referenceId: subscription._id,
361
+ referenceModel: 'Subscription',
307
362
  metadata: {
308
363
  ...metadata,
309
- subscriptionId: subscription._id.toString(),
364
+ subscriptionId: subscription._id.toString(), // Keep for backward compat
310
365
  entity: effectiveEntity,
311
366
  monetizationType: effectiveMonetizationType,
312
367
  isRenewal: true,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Commission Calculation Utility
3
+ * @classytic/revenue
4
+ *
5
+ * Handles platform commission calculation with gateway fee deduction
6
+ */
7
+
8
+ /**
9
+ * Build commission object for transaction
10
+ *
11
+ * @param {Number} amount - Transaction amount
12
+ * @param {Number} commissionRate - Commission rate (0 to 1, e.g., 0.10 for 10%)
13
+ * @param {Number} gatewayFeeRate - Gateway fee rate (0 to 1, e.g., 0.018 for 1.8%)
14
+ * @returns {Object} Commission object or null
15
+ */
16
+ export function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
17
+ // No commission if rate is 0 or negative
18
+ if (!commissionRate || commissionRate <= 0) {
19
+ return null;
20
+ }
21
+
22
+ // Validate inputs
23
+ if (amount < 0) {
24
+ throw new Error('Transaction amount cannot be negative');
25
+ }
26
+
27
+ if (commissionRate < 0 || commissionRate > 1) {
28
+ throw new Error('Commission rate must be between 0 and 1');
29
+ }
30
+
31
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
32
+ throw new Error('Gateway fee rate must be between 0 and 1');
33
+ }
34
+
35
+ // Calculate commission
36
+ const grossAmount = Math.round(amount * commissionRate * 100) / 100; // Round to 2 decimals
37
+ const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
38
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
39
+
40
+ return {
41
+ rate: commissionRate,
42
+ grossAmount,
43
+ gatewayFeeRate,
44
+ gatewayFeeAmount,
45
+ netAmount,
46
+ status: 'pending',
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Reverse commission on refund (proportional)
52
+ *
53
+ * @param {Object} originalCommission - Original commission object
54
+ * @param {Number} originalAmount - Original transaction amount
55
+ * @param {Number} refundAmount - Amount being refunded
56
+ * @returns {Object} Reversed commission or null
57
+ */
58
+ export function reverseCommission(originalCommission, originalAmount, refundAmount) {
59
+ if (!originalCommission || !originalCommission.netAmount) {
60
+ return null;
61
+ }
62
+
63
+ // Calculate proportional refund
64
+ const refundRatio = refundAmount / originalAmount;
65
+ const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
66
+ const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
67
+ const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
68
+
69
+ return {
70
+ rate: originalCommission.rate,
71
+ grossAmount: reversedGrossAmount,
72
+ gatewayFeeRate: originalCommission.gatewayFeeRate,
73
+ gatewayFeeAmount: reversedGatewayFee,
74
+ netAmount: reversedNetAmount,
75
+ status: 'waived', // Commission waived due to refund
76
+ };
77
+ }
78
+
79
+ export default {
80
+ calculateCommission,
81
+ reverseCommission,
82
+ };
83
+
package/utils/index.d.ts CHANGED
@@ -1,99 +1,124 @@
1
- /**
2
- * TypeScript definitions for @classytic/revenue/utils
3
- * Core utilities
4
- */
5
-
6
- // ============ TRANSACTION TYPE UTILITIES ============
7
-
8
- export const TRANSACTION_TYPE: {
9
- readonly MONETIZATION: 'monetization';
10
- readonly MANUAL: 'manual';
11
- };
12
-
13
- export const PROTECTED_MONETIZATION_FIELDS: readonly string[];
14
- export const EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: readonly string[];
15
- export const MANUAL_TRANSACTION_CREATE_FIELDS: readonly string[];
16
- export const MANUAL_TRANSACTION_UPDATE_FIELDS: readonly string[];
17
-
18
- export interface TransactionTypeOptions {
19
- targetModels?: string[];
20
- additionalCategories?: string[];
21
- }
22
-
23
- export function isMonetizationTransaction(
24
- transaction: any,
25
- options?: TransactionTypeOptions
26
- ): boolean;
27
-
28
- export function isManualTransaction(
29
- transaction: any,
30
- options?: TransactionTypeOptions
31
- ): boolean;
32
-
33
- export function getTransactionType(
34
- transaction: any,
35
- options?: TransactionTypeOptions
36
- ): 'monetization' | 'manual';
37
-
38
- export function getAllowedUpdateFields(
39
- transaction: any,
40
- options?: TransactionTypeOptions
41
- ): string[];
42
-
43
- export interface FieldValidationResult {
44
- allowed: boolean;
45
- reason?: string;
46
- }
47
-
48
- export function validateFieldUpdate(
49
- transaction: any,
50
- fieldName: string,
51
- options?: TransactionTypeOptions
52
- ): FieldValidationResult;
53
-
54
- export function canSelfVerify(
55
- transaction: any,
56
- options?: TransactionTypeOptions
57
- ): boolean;
58
-
59
- // ============ LOGGER UTILITIES ============
60
-
61
- export interface Logger {
62
- info(...args: any[]): void;
63
- warn(...args: any[]): void;
64
- error(...args: any[]): void;
65
- debug(...args: any[]): void;
66
- }
67
-
68
- export const logger: Logger;
69
- export function setLogger(logger: Logger | Console): void;
70
-
71
- // ============ HOOK UTILITIES ============
72
-
73
- export function triggerHook(
74
- hooks: Record<string, Function[]>,
75
- event: string,
76
- data: any,
77
- logger: Logger
78
- ): void;
79
-
80
- // ============ DEFAULT EXPORT ============
81
-
82
- declare const _default: {
83
- TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
84
- isMonetizationTransaction: typeof isMonetizationTransaction;
85
- isManualTransaction: typeof isManualTransaction;
86
- getTransactionType: typeof getTransactionType;
87
- PROTECTED_MONETIZATION_FIELDS: typeof PROTECTED_MONETIZATION_FIELDS;
88
- EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: typeof EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION;
89
- MANUAL_TRANSACTION_CREATE_FIELDS: typeof MANUAL_TRANSACTION_CREATE_FIELDS;
90
- MANUAL_TRANSACTION_UPDATE_FIELDS: typeof MANUAL_TRANSACTION_UPDATE_FIELDS;
91
- getAllowedUpdateFields: typeof getAllowedUpdateFields;
92
- validateFieldUpdate: typeof validateFieldUpdate;
93
- canSelfVerify: typeof canSelfVerify;
94
- logger: typeof logger;
95
- setLogger: typeof setLogger;
96
- triggerHook: typeof triggerHook;
97
- };
98
-
99
- export default _default;
1
+ /**
2
+ * TypeScript definitions for @classytic/revenue/utils
3
+ * Core utilities
4
+ */
5
+
6
+ // ============ TRANSACTION TYPE UTILITIES ============
7
+
8
+ export const TRANSACTION_TYPE: {
9
+ readonly MONETIZATION: 'monetization';
10
+ readonly MANUAL: 'manual';
11
+ };
12
+
13
+ export const PROTECTED_MONETIZATION_FIELDS: readonly string[];
14
+ export const EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: readonly string[];
15
+ export const MANUAL_TRANSACTION_CREATE_FIELDS: readonly string[];
16
+ export const MANUAL_TRANSACTION_UPDATE_FIELDS: readonly string[];
17
+
18
+ export interface TransactionTypeOptions {
19
+ targetModels?: string[];
20
+ additionalCategories?: string[];
21
+ }
22
+
23
+ export function isMonetizationTransaction(
24
+ transaction: any,
25
+ options?: TransactionTypeOptions
26
+ ): boolean;
27
+
28
+ export function isManualTransaction(
29
+ transaction: any,
30
+ options?: TransactionTypeOptions
31
+ ): boolean;
32
+
33
+ export function getTransactionType(
34
+ transaction: any,
35
+ options?: TransactionTypeOptions
36
+ ): 'monetization' | 'manual';
37
+
38
+ export function getAllowedUpdateFields(
39
+ transaction: any,
40
+ options?: TransactionTypeOptions
41
+ ): string[];
42
+
43
+ export interface FieldValidationResult {
44
+ allowed: boolean;
45
+ reason?: string;
46
+ }
47
+
48
+ export function validateFieldUpdate(
49
+ transaction: any,
50
+ fieldName: string,
51
+ options?: TransactionTypeOptions
52
+ ): FieldValidationResult;
53
+
54
+ export function canSelfVerify(
55
+ transaction: any,
56
+ options?: TransactionTypeOptions
57
+ ): boolean;
58
+
59
+ // ============ LOGGER UTILITIES ============
60
+
61
+ export interface Logger {
62
+ info(...args: any[]): void;
63
+ warn(...args: any[]): void;
64
+ error(...args: any[]): void;
65
+ debug(...args: any[]): void;
66
+ }
67
+
68
+ export const logger: Logger;
69
+ export function setLogger(logger: Logger | Console): void;
70
+
71
+ // ============ HOOK UTILITIES ============
72
+
73
+ export function triggerHook(
74
+ hooks: Record<string, Function[]>,
75
+ event: string,
76
+ data: any,
77
+ logger: Logger
78
+ ): void;
79
+
80
+ // ============ COMMISSION UTILITIES ============
81
+
82
+ export interface CommissionObject {
83
+ rate: number;
84
+ grossAmount: number;
85
+ gatewayFeeRate: number;
86
+ gatewayFeeAmount: number;
87
+ netAmount: number;
88
+ status: 'pending' | 'due' | 'paid' | 'waived';
89
+ }
90
+
91
+ export function calculateCommission(
92
+ amount: number,
93
+ commissionRate: number,
94
+ gatewayFeeRate?: number
95
+ ): CommissionObject | null;
96
+
97
+ export function reverseCommission(
98
+ originalCommission: CommissionObject,
99
+ originalAmount: number,
100
+ refundAmount: number
101
+ ): CommissionObject | null;
102
+
103
+ // ============ DEFAULT EXPORT ============
104
+
105
+ declare const _default: {
106
+ TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
107
+ isMonetizationTransaction: typeof isMonetizationTransaction;
108
+ isManualTransaction: typeof isManualTransaction;
109
+ getTransactionType: typeof getTransactionType;
110
+ PROTECTED_MONETIZATION_FIELDS: typeof PROTECTED_MONETIZATION_FIELDS;
111
+ EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: typeof EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION;
112
+ MANUAL_TRANSACTION_CREATE_FIELDS: typeof MANUAL_TRANSACTION_CREATE_FIELDS;
113
+ MANUAL_TRANSACTION_UPDATE_FIELDS: typeof MANUAL_TRANSACTION_UPDATE_FIELDS;
114
+ getAllowedUpdateFields: typeof getAllowedUpdateFields;
115
+ validateFieldUpdate: typeof validateFieldUpdate;
116
+ canSelfVerify: typeof canSelfVerify;
117
+ logger: typeof logger;
118
+ setLogger: typeof setLogger;
119
+ triggerHook: typeof triggerHook;
120
+ calculateCommission: typeof calculateCommission;
121
+ reverseCommission: typeof reverseCommission;
122
+ };
123
+
124
+ export default _default;
package/utils/index.js CHANGED
@@ -1,8 +1,9 @@
1
- /**
2
- * Core Utilities
3
- * @classytic/revenue
4
- */
5
-
6
- export * from './transaction-type.js';
7
- export { default as logger, setLogger } from './logger.js';
8
- export { triggerHook } from './hooks.js';
1
+ /**
2
+ * Core Utilities
3
+ * @classytic/revenue
4
+ */
5
+
6
+ export * from './transaction-type.js';
7
+ export { default as logger, setLogger } from './logger.js';
8
+ export { triggerHook } from './hooks.js';
9
+ export { calculateCommission, reverseCommission } from './commission.js';