@classytic/revenue 0.0.22 → 0.0.24
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 +526 -380
- package/index.js +12 -0
- package/package.json +1 -1
- package/revenue.d.ts +350 -320
- package/services/payment.service.js +441 -431
- package/services/subscription.service.js +44 -4
- package/utils/commission.js +83 -0
- package/utils/index.d.ts +164 -99
- package/utils/index.js +10 -8
- package/utils/subscription/actions.js +68 -0
- package/utils/subscription/index.js +20 -0
- package/utils/subscription/period.js +123 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from '../core/errors.js';
|
|
20
20
|
import { triggerHook } from '../utils/hooks.js';
|
|
21
21
|
import { resolveCategory } from '../utils/category-resolver.js';
|
|
22
|
+
import { calculateCommission } from '../utils/commission.js';
|
|
22
23
|
import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
|
|
23
24
|
import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
|
|
24
25
|
|
|
@@ -40,7 +41,7 @@ export class SubscriptionService {
|
|
|
40
41
|
* Create a new subscription
|
|
41
42
|
*
|
|
42
43
|
* @param {Object} params - Subscription parameters
|
|
43
|
-
* @param {Object} params.data - Subscription data (organizationId, customerId, etc.)
|
|
44
|
+
* @param {Object} params.data - Subscription data (organizationId, customerId, referenceId, referenceModel, etc.)
|
|
44
45
|
* @param {String} params.planKey - Plan key ('monthly', 'quarterly', 'yearly')
|
|
45
46
|
* @param {Number} params.amount - Subscription amount
|
|
46
47
|
* @param {String} params.currency - Currency code (default: 'BDT')
|
|
@@ -51,6 +52,20 @@ export class SubscriptionService {
|
|
|
51
52
|
* @param {Object} params.paymentData - Payment method details
|
|
52
53
|
* @param {Object} params.metadata - Additional metadata
|
|
53
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
|
+
*
|
|
54
69
|
* @returns {Promise<Object>} { subscription, transaction, paymentIntent }
|
|
55
70
|
*/
|
|
56
71
|
async create(params) {
|
|
@@ -116,6 +131,11 @@ export class SubscriptionService {
|
|
|
116
131
|
|| this.config.transactionTypeMapping?.[monetizationType]
|
|
117
132
|
|| TRANSACTION_TYPE.INCOME;
|
|
118
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
|
+
|
|
119
139
|
// Create transaction record
|
|
120
140
|
const TransactionModel = this.models.Transaction;
|
|
121
141
|
transaction = await TransactionModel.create({
|
|
@@ -136,6 +156,10 @@ export class SubscriptionService {
|
|
|
136
156
|
provider: gateway,
|
|
137
157
|
...paymentData,
|
|
138
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 }),
|
|
139
163
|
metadata: {
|
|
140
164
|
...metadata,
|
|
141
165
|
planKey,
|
|
@@ -152,7 +176,8 @@ export class SubscriptionService {
|
|
|
152
176
|
if (this.models.Subscription) {
|
|
153
177
|
const SubscriptionModel = this.models.Subscription;
|
|
154
178
|
|
|
155
|
-
subscription
|
|
179
|
+
// Create subscription with proper reference tracking
|
|
180
|
+
const subscriptionData = {
|
|
156
181
|
organizationId: data.organizationId,
|
|
157
182
|
customerId: data.customerId || null,
|
|
158
183
|
planKey,
|
|
@@ -170,7 +195,13 @@ export class SubscriptionService {
|
|
|
170
195
|
monetizationType,
|
|
171
196
|
},
|
|
172
197
|
...data,
|
|
173
|
-
}
|
|
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);
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
// Trigger hook
|
|
@@ -299,6 +330,11 @@ export class SubscriptionService {
|
|
|
299
330
|
|| this.config.transactionTypeMapping?.[effectiveMonetizationType]
|
|
300
331
|
|| TRANSACTION_TYPE.INCOME;
|
|
301
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
|
+
|
|
302
338
|
// Create transaction
|
|
303
339
|
const TransactionModel = this.models.Transaction;
|
|
304
340
|
const transaction = await TransactionModel.create({
|
|
@@ -319,9 +355,13 @@ export class SubscriptionService {
|
|
|
319
355
|
provider: gateway,
|
|
320
356
|
...paymentData,
|
|
321
357
|
},
|
|
358
|
+
...(commission && { commission }), // Only include if commission exists
|
|
359
|
+
// Polymorphic reference to subscription
|
|
360
|
+
referenceId: subscription._id,
|
|
361
|
+
referenceModel: 'Subscription',
|
|
322
362
|
metadata: {
|
|
323
363
|
...metadata,
|
|
324
|
-
subscriptionId: subscription._id.toString(),
|
|
364
|
+
subscriptionId: subscription._id.toString(), // Keep for backward compat
|
|
325
365
|
entity: effectiveEntity,
|
|
326
366
|
monetizationType: effectiveMonetizationType,
|
|
327
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,164 @@
|
|
|
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
|
-
// ============
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
// ============ SUBSCRIPTION UTILITIES ============
|
|
104
|
+
|
|
105
|
+
export function addDuration(startDate: Date, duration: number, unit?: string): Date;
|
|
106
|
+
|
|
107
|
+
export function calculatePeriodRange(params: {
|
|
108
|
+
currentEndDate?: Date | null;
|
|
109
|
+
startDate?: Date | null;
|
|
110
|
+
duration: number;
|
|
111
|
+
unit?: string;
|
|
112
|
+
now?: Date;
|
|
113
|
+
}): { startDate: Date; endDate: Date };
|
|
114
|
+
|
|
115
|
+
export function calculateProratedAmount(params: {
|
|
116
|
+
amountPaid: number;
|
|
117
|
+
startDate: Date;
|
|
118
|
+
endDate: Date;
|
|
119
|
+
asOfDate?: Date;
|
|
120
|
+
precision?: number;
|
|
121
|
+
}): number;
|
|
122
|
+
|
|
123
|
+
export function resolveIntervalToDuration(
|
|
124
|
+
interval?: string,
|
|
125
|
+
intervalCount?: number
|
|
126
|
+
): { duration: number; unit: string };
|
|
127
|
+
|
|
128
|
+
export function isSubscriptionActive(subscription: any): boolean;
|
|
129
|
+
export function canRenewSubscription(entity: any): boolean;
|
|
130
|
+
export function canCancelSubscription(entity: any): boolean;
|
|
131
|
+
export function canPauseSubscription(entity: any): boolean;
|
|
132
|
+
export function canResumeSubscription(entity: any): boolean;
|
|
133
|
+
|
|
134
|
+
// ============ DEFAULT EXPORT ============
|
|
135
|
+
|
|
136
|
+
declare const _default: {
|
|
137
|
+
TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
|
|
138
|
+
isMonetizationTransaction: typeof isMonetizationTransaction;
|
|
139
|
+
isManualTransaction: typeof isManualTransaction;
|
|
140
|
+
getTransactionType: typeof getTransactionType;
|
|
141
|
+
PROTECTED_MONETIZATION_FIELDS: typeof PROTECTED_MONETIZATION_FIELDS;
|
|
142
|
+
EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: typeof EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION;
|
|
143
|
+
MANUAL_TRANSACTION_CREATE_FIELDS: typeof MANUAL_TRANSACTION_CREATE_FIELDS;
|
|
144
|
+
MANUAL_TRANSACTION_UPDATE_FIELDS: typeof MANUAL_TRANSACTION_UPDATE_FIELDS;
|
|
145
|
+
getAllowedUpdateFields: typeof getAllowedUpdateFields;
|
|
146
|
+
validateFieldUpdate: typeof validateFieldUpdate;
|
|
147
|
+
canSelfVerify: typeof canSelfVerify;
|
|
148
|
+
logger: typeof logger;
|
|
149
|
+
setLogger: typeof setLogger;
|
|
150
|
+
triggerHook: typeof triggerHook;
|
|
151
|
+
calculateCommission: typeof calculateCommission;
|
|
152
|
+
reverseCommission: typeof reverseCommission;
|
|
153
|
+
addDuration: typeof addDuration;
|
|
154
|
+
calculatePeriodRange: typeof calculatePeriodRange;
|
|
155
|
+
calculateProratedAmount: typeof calculateProratedAmount;
|
|
156
|
+
resolveIntervalToDuration: typeof resolveIntervalToDuration;
|
|
157
|
+
isSubscriptionActive: typeof isSubscriptionActive;
|
|
158
|
+
canRenewSubscription: typeof canRenewSubscription;
|
|
159
|
+
canCancelSubscription: typeof canCancelSubscription;
|
|
160
|
+
canPauseSubscription: typeof canPauseSubscription;
|
|
161
|
+
canResumeSubscription: typeof canResumeSubscription;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default _default;
|
package/utils/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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';
|
|
10
|
+
export * from './subscription/index.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Action Utilities
|
|
3
|
+
* @classytic/revenue/utils/subscription
|
|
4
|
+
*
|
|
5
|
+
* Eligibility checks for subscription actions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { SUBSCRIPTION_STATUS } from '../../enums/subscription.enums.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if subscription is active
|
|
12
|
+
*/
|
|
13
|
+
export function isSubscriptionActive(subscription) {
|
|
14
|
+
if (!subscription) return false;
|
|
15
|
+
if (!subscription.isActive) return false;
|
|
16
|
+
|
|
17
|
+
if (subscription.endDate) {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const endDate = new Date(subscription.endDate);
|
|
20
|
+
if (endDate < now) return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if can renew
|
|
28
|
+
*/
|
|
29
|
+
export function canRenewSubscription(entity) {
|
|
30
|
+
if (!entity || !entity.subscription) return false;
|
|
31
|
+
return isSubscriptionActive(entity.subscription);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if can cancel
|
|
36
|
+
*/
|
|
37
|
+
export function canCancelSubscription(entity) {
|
|
38
|
+
if (!entity || !entity.subscription) return false;
|
|
39
|
+
if (!isSubscriptionActive(entity.subscription)) return false;
|
|
40
|
+
return !entity.subscription.canceledAt;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if can pause
|
|
45
|
+
*/
|
|
46
|
+
export function canPauseSubscription(entity) {
|
|
47
|
+
if (!entity || !entity.subscription) return false;
|
|
48
|
+
if (entity.status === SUBSCRIPTION_STATUS.PAUSED) return false;
|
|
49
|
+
if (entity.status === SUBSCRIPTION_STATUS.CANCELLED) return false;
|
|
50
|
+
return isSubscriptionActive(entity.subscription);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if can resume
|
|
55
|
+
*/
|
|
56
|
+
export function canResumeSubscription(entity) {
|
|
57
|
+
if (!entity || !entity.subscription) return false;
|
|
58
|
+
return entity.status === SUBSCRIPTION_STATUS.PAUSED;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
isSubscriptionActive,
|
|
63
|
+
canRenewSubscription,
|
|
64
|
+
canCancelSubscription,
|
|
65
|
+
canPauseSubscription,
|
|
66
|
+
canResumeSubscription,
|
|
67
|
+
};
|
|
68
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Utilities Index
|
|
3
|
+
* @classytic/revenue/utils/subscription
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
addDuration,
|
|
8
|
+
calculatePeriodRange,
|
|
9
|
+
calculateProratedAmount,
|
|
10
|
+
resolveIntervalToDuration,
|
|
11
|
+
} from './period.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
isSubscriptionActive,
|
|
15
|
+
canRenewSubscription,
|
|
16
|
+
canCancelSubscription,
|
|
17
|
+
canPauseSubscription,
|
|
18
|
+
canResumeSubscription,
|
|
19
|
+
} from './actions.js';
|
|
20
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Period Utilities
|
|
3
|
+
* @classytic/revenue/utils/subscription
|
|
4
|
+
*
|
|
5
|
+
* Universal period calculation, proration, and date utilities
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Add duration to date
|
|
10
|
+
*/
|
|
11
|
+
export function addDuration(startDate, duration, unit = 'days') {
|
|
12
|
+
const date = new Date(startDate);
|
|
13
|
+
|
|
14
|
+
switch (unit) {
|
|
15
|
+
case 'months':
|
|
16
|
+
case 'month':
|
|
17
|
+
date.setMonth(date.getMonth() + duration);
|
|
18
|
+
return date;
|
|
19
|
+
case 'years':
|
|
20
|
+
case 'year':
|
|
21
|
+
date.setFullYear(date.getFullYear() + duration);
|
|
22
|
+
return date;
|
|
23
|
+
case 'weeks':
|
|
24
|
+
case 'week':
|
|
25
|
+
date.setDate(date.getDate() + (duration * 7));
|
|
26
|
+
return date;
|
|
27
|
+
case 'days':
|
|
28
|
+
case 'day':
|
|
29
|
+
default:
|
|
30
|
+
date.setDate(date.getDate() + duration);
|
|
31
|
+
return date;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate subscription period start/end dates
|
|
37
|
+
*/
|
|
38
|
+
export function calculatePeriodRange({
|
|
39
|
+
currentEndDate = null,
|
|
40
|
+
startDate = null,
|
|
41
|
+
duration,
|
|
42
|
+
unit = 'days',
|
|
43
|
+
now = new Date(),
|
|
44
|
+
}) {
|
|
45
|
+
let periodStart;
|
|
46
|
+
|
|
47
|
+
if (startDate) {
|
|
48
|
+
periodStart = new Date(startDate);
|
|
49
|
+
} else if (currentEndDate) {
|
|
50
|
+
const end = new Date(currentEndDate);
|
|
51
|
+
periodStart = end > now ? end : now;
|
|
52
|
+
} else {
|
|
53
|
+
periodStart = now;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const periodEnd = addDuration(periodStart, duration, unit);
|
|
57
|
+
|
|
58
|
+
return { startDate: periodStart, endDate: periodEnd };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate prorated refund amount for unused period
|
|
63
|
+
*/
|
|
64
|
+
export function calculateProratedAmount({
|
|
65
|
+
amountPaid,
|
|
66
|
+
startDate,
|
|
67
|
+
endDate,
|
|
68
|
+
asOfDate = new Date(),
|
|
69
|
+
precision = 2,
|
|
70
|
+
}) {
|
|
71
|
+
if (!amountPaid || amountPaid <= 0) return 0;
|
|
72
|
+
|
|
73
|
+
const start = new Date(startDate);
|
|
74
|
+
const end = new Date(endDate);
|
|
75
|
+
const asOf = new Date(asOfDate);
|
|
76
|
+
|
|
77
|
+
const totalMs = end - start;
|
|
78
|
+
if (totalMs <= 0) return 0;
|
|
79
|
+
|
|
80
|
+
const remainingMs = Math.max(0, end - asOf);
|
|
81
|
+
if (remainingMs <= 0) return 0;
|
|
82
|
+
|
|
83
|
+
const ratio = remainingMs / totalMs;
|
|
84
|
+
const amount = amountPaid * ratio;
|
|
85
|
+
|
|
86
|
+
const factor = 10 ** precision;
|
|
87
|
+
return Math.round(amount * factor) / factor;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert interval + count to duration/unit
|
|
92
|
+
*/
|
|
93
|
+
export function resolveIntervalToDuration(interval = 'month', intervalCount = 1) {
|
|
94
|
+
const normalized = (interval || 'month').toLowerCase();
|
|
95
|
+
const count = Number(intervalCount) > 0 ? Number(intervalCount) : 1;
|
|
96
|
+
|
|
97
|
+
switch (normalized) {
|
|
98
|
+
case 'year':
|
|
99
|
+
case 'years':
|
|
100
|
+
return { duration: count, unit: 'years' };
|
|
101
|
+
case 'week':
|
|
102
|
+
case 'weeks':
|
|
103
|
+
return { duration: count, unit: 'weeks' };
|
|
104
|
+
case 'quarter':
|
|
105
|
+
case 'quarters':
|
|
106
|
+
return { duration: count * 3, unit: 'months' };
|
|
107
|
+
case 'day':
|
|
108
|
+
case 'days':
|
|
109
|
+
return { duration: count, unit: 'days' };
|
|
110
|
+
case 'month':
|
|
111
|
+
case 'months':
|
|
112
|
+
default:
|
|
113
|
+
return { duration: count, unit: 'months' };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default {
|
|
118
|
+
addDuration,
|
|
119
|
+
calculatePeriodRange,
|
|
120
|
+
calculateProratedAmount,
|
|
121
|
+
resolveIntervalToDuration,
|
|
122
|
+
};
|
|
123
|
+
|