@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,180 @@
1
+ /**
2
+ * Commission Split Utilities
3
+ * @classytic/revenue
4
+ *
5
+ * Multi-party commission split calculation for affiliate/referral systems
6
+ */
7
+
8
+ import { SPLIT_TYPE, SPLIT_STATUS } from '../enums/split.enums.js';
9
+
10
+ /**
11
+ * Calculate multi-party commission splits
12
+ *
13
+ * @param {Number} amount - Transaction amount
14
+ * @param {Array} splitRules - Split configuration
15
+ * @param {Number} gatewayFeeRate - Gateway fee rate (optional)
16
+ * @returns {Array} Split objects
17
+ *
18
+ * @example
19
+ * calculateSplits(1000, [
20
+ * { type: 'platform_commission', recipientId: 'platform', recipientType: 'platform', rate: 0.10 },
21
+ * { type: 'affiliate_commission', recipientId: 'affiliate-123', recipientType: 'user', rate: 0.02 },
22
+ * ], 0.018);
23
+ *
24
+ * Returns:
25
+ * [
26
+ * { type: 'platform_commission', recipientId: 'platform', grossAmount: 100, gatewayFeeAmount: 18, netAmount: 82, ... },
27
+ * { type: 'affiliate_commission', recipientId: 'affiliate-123', grossAmount: 20, gatewayFeeAmount: 0, netAmount: 20, ... },
28
+ * ]
29
+ */
30
+ export function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
31
+ if (!splitRules || splitRules.length === 0) {
32
+ return [];
33
+ }
34
+
35
+ if (amount < 0) {
36
+ throw new Error('Transaction amount cannot be negative');
37
+ }
38
+
39
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
40
+ throw new Error('Gateway fee rate must be between 0 and 1');
41
+ }
42
+
43
+ const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
44
+ if (totalRate > 1) {
45
+ throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
46
+ }
47
+
48
+ return splitRules.map((rule, index) => {
49
+ if (rule.rate < 0 || rule.rate > 1) {
50
+ throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
51
+ }
52
+
53
+ const grossAmount = Math.round(amount * rule.rate * 100) / 100;
54
+
55
+ const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0
56
+ ? Math.round(amount * gatewayFeeRate * 100) / 100
57
+ : 0;
58
+
59
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
60
+
61
+ return {
62
+ type: rule.type || SPLIT_TYPE.CUSTOM,
63
+ recipientId: rule.recipientId,
64
+ recipientType: rule.recipientType,
65
+ rate: rule.rate,
66
+ grossAmount,
67
+ gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
68
+ gatewayFeeAmount,
69
+ netAmount,
70
+ status: SPLIT_STATUS.PENDING,
71
+ dueDate: rule.dueDate || null,
72
+ metadata: rule.metadata || {},
73
+ };
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Calculate organization payout after splits
79
+ *
80
+ * @param {Number} amount - Total transaction amount
81
+ * @param {Array} splits - Calculated splits
82
+ * @returns {Number} Amount organization receives
83
+ */
84
+ export function calculateOrganizationPayout(amount, splits = []) {
85
+ const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
86
+ return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
87
+ }
88
+
89
+ /**
90
+ * Reverse splits proportionally on refund
91
+ *
92
+ * @param {Array} originalSplits - Original split objects
93
+ * @param {Number} originalAmount - Original transaction amount
94
+ * @param {Number} refundAmount - Amount being refunded
95
+ * @returns {Array} Reversed splits
96
+ */
97
+ export function reverseSplits(originalSplits, originalAmount, refundAmount) {
98
+ if (!originalSplits || originalSplits.length === 0) {
99
+ return [];
100
+ }
101
+
102
+ const refundRatio = refundAmount / originalAmount;
103
+
104
+ return originalSplits.map(split => ({
105
+ ...split,
106
+ grossAmount: Math.round(split.grossAmount * refundRatio * 100) / 100,
107
+ gatewayFeeAmount: Math.round(split.gatewayFeeAmount * refundRatio * 100) / 100,
108
+ netAmount: Math.round(split.netAmount * refundRatio * 100) / 100,
109
+ status: SPLIT_STATUS.WAIVED,
110
+ }));
111
+ }
112
+
113
+ /**
114
+ * Build commission object with splits support
115
+ * Backward compatible with existing calculateCommission
116
+ *
117
+ * @param {Number} amount - Transaction amount
118
+ * @param {Number} commissionRate - Platform commission rate
119
+ * @param {Number} gatewayFeeRate - Gateway fee rate
120
+ * @param {Object} options - Additional options
121
+ * @returns {Object} Commission with optional splits
122
+ */
123
+ export function calculateCommissionWithSplits(amount, commissionRate, gatewayFeeRate = 0, options = {}) {
124
+ const { affiliateRate = 0, affiliateId = null, affiliateType = 'user' } = options;
125
+
126
+ if (commissionRate <= 0 && affiliateRate <= 0) {
127
+ return null;
128
+ }
129
+
130
+ const splitRules = [];
131
+
132
+ if (commissionRate > 0) {
133
+ splitRules.push({
134
+ type: SPLIT_TYPE.PLATFORM_COMMISSION,
135
+ recipientId: 'platform',
136
+ recipientType: 'platform',
137
+ rate: commissionRate,
138
+ });
139
+ }
140
+
141
+ if (affiliateRate > 0 && affiliateId) {
142
+ splitRules.push({
143
+ type: SPLIT_TYPE.AFFILIATE_COMMISSION,
144
+ recipientId: affiliateId,
145
+ recipientType: affiliateType,
146
+ rate: affiliateRate,
147
+ });
148
+ }
149
+
150
+ const splits = calculateSplits(amount, splitRules, gatewayFeeRate);
151
+
152
+ const platformSplit = splits.find(s => s.type === SPLIT_TYPE.PLATFORM_COMMISSION);
153
+ const affiliateSplit = splits.find(s => s.type === SPLIT_TYPE.AFFILIATE_COMMISSION);
154
+
155
+ return {
156
+ rate: commissionRate,
157
+ grossAmount: platformSplit?.grossAmount || 0,
158
+ gatewayFeeRate: platformSplit?.gatewayFeeRate || 0,
159
+ gatewayFeeAmount: platformSplit?.gatewayFeeAmount || 0,
160
+ netAmount: platformSplit?.netAmount || 0,
161
+ status: SPLIT_STATUS.PENDING,
162
+ ...(splits.length > 0 && { splits }),
163
+ ...(affiliateSplit && {
164
+ affiliate: {
165
+ recipientId: affiliateSplit.recipientId,
166
+ recipientType: affiliateSplit.recipientType,
167
+ rate: affiliateSplit.rate,
168
+ grossAmount: affiliateSplit.grossAmount,
169
+ netAmount: affiliateSplit.netAmount,
170
+ },
171
+ }),
172
+ };
173
+ }
174
+
175
+ export default {
176
+ calculateSplits,
177
+ calculateOrganizationPayout,
178
+ reverseSplits,
179
+ calculateCommissionWithSplits,
180
+ };
package/utils/index.js CHANGED
@@ -7,4 +7,10 @@ export * from './transaction-type.js';
7
7
  export { default as logger, setLogger } from './logger.js';
8
8
  export { triggerHook } from './hooks.js';
9
9
  export { calculateCommission, reverseCommission } from './commission.js';
10
+ export {
11
+ calculateSplits,
12
+ calculateOrganizationPayout,
13
+ reverseSplits,
14
+ calculateCommissionWithSplits,
15
+ } from './commission-split.js';
10
16
  export * from './subscription/index.js';
package/revenue.d.ts DELETED
@@ -1,350 +0,0 @@
1
- /**
2
- * TypeScript definitions for @classytic/revenue
3
- * Enterprise Revenue Management System
4
- *
5
- * Thin, focused, production-ready library with smart defaults.
6
- *
7
- * @version 1.0.0
8
- */
9
-
10
- import { Schema, Model, Document } from 'mongoose';
11
-
12
- // ============ CORE API ============
13
-
14
- // Container
15
- export class Container {
16
- register(name: string, implementation: any, options?: { singleton?: boolean; factory?: boolean }): this;
17
- singleton(name: string, implementation: any): this;
18
- transient(name: string, factory: Function): this;
19
- get(name: string): any;
20
- has(name: string): boolean;
21
- keys(): string[];
22
- clear(): void;
23
- createScope(): Container;
24
- }
25
-
26
- // Provider System
27
- export interface PaymentIntentParams {
28
- amount: number;
29
- currency?: string;
30
- metadata?: Record<string, any>;
31
- }
32
-
33
- export class PaymentIntent {
34
- id: string;
35
- provider: string;
36
- status: string;
37
- amount: number;
38
- currency: string;
39
- metadata: Record<string, any>;
40
- clientSecret?: string;
41
- paymentUrl?: string;
42
- instructions?: string;
43
- raw?: any;
44
- }
45
-
46
- export class PaymentResult {
47
- id: string;
48
- provider: string;
49
- status: string;
50
- amount: number;
51
- currency: string;
52
- paidAt?: Date;
53
- metadata: Record<string, any>;
54
- raw?: any;
55
- }
56
-
57
- export class RefundResult {
58
- id: string;
59
- provider: string;
60
- status: string;
61
- amount: number;
62
- currency: string;
63
- refundedAt?: Date;
64
- reason?: string;
65
- metadata: Record<string, any>;
66
- raw?: any;
67
- }
68
-
69
- export class WebhookEvent {
70
- id: string;
71
- provider: string;
72
- type: string;
73
- data: any;
74
- createdAt: Date;
75
- raw?: any;
76
- }
77
-
78
- export abstract class PaymentProvider {
79
- name: string;
80
- config: any;
81
-
82
- createIntent(params: PaymentIntentParams): Promise<PaymentIntent>;
83
- verifyPayment(intentId: string): Promise<PaymentResult>;
84
- getStatus(intentId: string): Promise<PaymentResult>;
85
- refund(paymentId: string, amount?: number, options?: any): Promise<RefundResult>;
86
- handleWebhook(payload: any, headers?: any): Promise<WebhookEvent>;
87
- verifyWebhookSignature(payload: any, signature: string): boolean;
88
- getCapabilities(): {
89
- supportsWebhooks: boolean;
90
- supportsRefunds: boolean;
91
- supportsPartialRefunds: boolean;
92
- requiresManualVerification: boolean;
93
- };
94
- }
95
-
96
- // Note: ManualProvider moved to @classytic/revenue-manual (separate package)
97
-
98
- // Services
99
- export class SubscriptionService {
100
- constructor(container: Container);
101
-
102
- create(params: {
103
- data: any;
104
- planKey: string;
105
- amount: number;
106
- currency?: string;
107
- gateway?: string;
108
- entity?: string;
109
- monetizationType?: 'free' | 'subscription' | 'purchase';
110
- paymentData?: any;
111
- metadata?: Record<string, any>;
112
- idempotencyKey?: string;
113
- }): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent | null }>;
114
-
115
- activate(subscriptionId: string, options?: { timestamp?: Date }): Promise<any>;
116
- renew(subscriptionId: string, params?: {
117
- gateway?: string;
118
- entity?: string;
119
- paymentData?: any;
120
- metadata?: Record<string, any>;
121
- idempotencyKey?: string;
122
- }): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent }>;
123
- cancel(subscriptionId: string, options?: { immediate?: boolean; reason?: string }): Promise<any>;
124
- pause(subscriptionId: string, options?: { reason?: string }): Promise<any>;
125
- resume(subscriptionId: string, options?: { extendPeriod?: boolean }): Promise<any>;
126
- list(filters?: any, options?: any): Promise<any[]>;
127
- get(subscriptionId: string): Promise<any>;
128
- }
129
-
130
- export class PaymentService {
131
- constructor(container: Container);
132
-
133
- verify(paymentIntentId: string, options?: { verifiedBy?: string }): Promise<{ transaction: any; paymentResult: PaymentResult; status: string }>;
134
- getStatus(paymentIntentId: string): Promise<{ transaction: any; paymentResult: PaymentResult; status: string; provider: string }>;
135
- refund(paymentId: string, amount?: number, options?: { reason?: string }): Promise<{ transaction: any; refundTransaction: any; refundResult: RefundResult; status: string }>;
136
- handleWebhook(providerName: string, payload: any, headers?: any): Promise<{ event: WebhookEvent; transaction: any; status: string }>;
137
- list(filters?: any, options?: any): Promise<any[]>;
138
- get(transactionId: string): Promise<any>;
139
- getProvider(providerName: string): PaymentProvider;
140
- }
141
-
142
- export class TransactionService {
143
- constructor(container: Container);
144
-
145
- get(transactionId: string): Promise<any>;
146
- list(filters?: any, options?: any): Promise<{ transactions: any[]; total: number; page: number; limit: number; pages: number }>;
147
- update(transactionId: string, updates: any): Promise<any>;
148
- }
149
-
150
- // Error Classes
151
- export class RevenueError extends Error {
152
- code: string;
153
- retryable: boolean;
154
- metadata: Record<string, any>;
155
- toJSON(): { name: string; message: string; code: string; retryable: boolean; metadata: Record<string, any> };
156
- }
157
-
158
- export class ConfigurationError extends RevenueError {}
159
- export class ModelNotRegisteredError extends ConfigurationError {}
160
- export class ProviderError extends RevenueError {}
161
- export class ProviderNotFoundError extends ProviderError {}
162
- export class ProviderCapabilityError extends ProviderError {}
163
- export class PaymentIntentCreationError extends ProviderError {}
164
- export class PaymentVerificationError extends ProviderError {}
165
- export class NotFoundError extends RevenueError {}
166
- export class SubscriptionNotFoundError extends NotFoundError {}
167
- export class TransactionNotFoundError extends NotFoundError {}
168
- export class ValidationError extends RevenueError {}
169
- export class InvalidAmountError extends ValidationError {}
170
- export class MissingRequiredFieldError extends ValidationError {}
171
- export class StateError extends RevenueError {}
172
- export class AlreadyVerifiedError extends StateError {}
173
- export class InvalidStateTransitionError extends StateError {}
174
- export class SubscriptionNotActiveError extends StateError {}
175
- export class OperationError extends RevenueError {}
176
- export class RefundNotSupportedError extends OperationError {}
177
- export class RefundError extends OperationError {}
178
-
179
- export function isRetryable(error: Error): boolean;
180
- export function isRevenueError(error: Error): boolean;
181
-
182
- // Revenue Instance (Immutable)
183
- export interface Revenue {
184
- readonly container: Container;
185
- readonly providers: Readonly<Record<string, PaymentProvider>>;
186
- readonly config: Readonly<any>;
187
-
188
- readonly subscriptions: SubscriptionService;
189
- readonly payments: PaymentService;
190
- readonly transactions: TransactionService;
191
-
192
- getProvider(name: string): PaymentProvider;
193
- }
194
-
195
- export interface RevenueOptions {
196
- models: {
197
- Transaction: Model<any>;
198
- Subscription?: Model<any>;
199
- [key: string]: Model<any> | undefined;
200
- };
201
- providers?: Record<string, PaymentProvider>;
202
- hooks?: Record<string, Function[]>;
203
- config?: {
204
- /**
205
- * Maps logical entity identifiers to custom transaction category names
206
- *
207
- * Entity identifiers are NOT database model names - they are logical identifiers
208
- * you choose to organize your business logic.
209
- *
210
- * @example
211
- * categoryMappings: {
212
- * Order: 'order_subscription', // Customer orders
213
- * PlatformSubscription: 'platform_subscription', // Tenant/org subscriptions
214
- * TenantUpgrade: 'tenant_upgrade', // Tenant upgrades
215
- * Membership: 'gym_membership', // User memberships
216
- * Enrollment: 'course_enrollment', // Course enrollments
217
- * }
218
- *
219
- * If not specified, falls back to library defaults: 'subscription' or 'purchase'
220
- */
221
- categoryMappings?: Record<string, string>;
222
-
223
- /**
224
- * Maps transaction types to income/expense for your accounting system
225
- *
226
- * Allows you to control how different transaction types are recorded:
227
- * - 'income': Money coming in (payments, subscriptions)
228
- * - 'expense': Money going out (refunds)
229
- *
230
- * @example
231
- * transactionTypeMapping: {
232
- * subscription: 'income',
233
- * subscription_renewal: 'income',
234
- * purchase: 'income',
235
- * refund: 'expense',
236
- * }
237
- *
238
- * If not specified, library defaults to 'income' for all payment transactions
239
- */
240
- transactionTypeMapping?: Record<string, 'income' | 'expense'>;
241
-
242
- /**
243
- * Commission rates by category (0 to 1)
244
- *
245
- * Automatically calculates platform commission for each transaction.
246
- * Gateway fees are automatically deducted from gross commission.
247
- *
248
- * @example
249
- * commissionRates: {
250
- * 'course_enrollment': 0.10, // 10% commission
251
- * 'product_order': 0.05, // 5% commission
252
- * 'gym_membership': 0, // No commission
253
- * }
254
- */
255
- commissionRates?: Record<string, number>;
256
-
257
- /**
258
- * Gateway fee rates by provider (0 to 1)
259
- *
260
- * Gateway fees are deducted from gross commission.
261
- *
262
- * @example
263
- * gatewayFeeRates: {
264
- * 'bkash': 0.018, // 1.8% fee
265
- * 'stripe': 0.029, // 2.9% fee
266
- * 'manual': 0, // No fee
267
- * }
268
- */
269
- gatewayFeeRates?: Record<string, number>;
270
-
271
- [key: string]: any;
272
- };
273
- logger?: Console | any;
274
- }
275
-
276
- export function createRevenue(options: RevenueOptions): Revenue;
277
-
278
- // ============ ENUMS ============
279
-
280
- export const TRANSACTION_TYPE: {
281
- INCOME: 'income';
282
- EXPENSE: 'expense';
283
- };
284
-
285
- export const TRANSACTION_STATUS: {
286
- PENDING: 'pending';
287
- PAYMENT_INITIATED: 'payment_initiated';
288
- PROCESSING: 'processing';
289
- REQUIRES_ACTION: 'requires_action';
290
- VERIFIED: 'verified';
291
- COMPLETED: 'completed';
292
- FAILED: 'failed';
293
- CANCELLED: 'cancelled';
294
- EXPIRED: 'expired';
295
- REFUNDED: 'refunded';
296
- PARTIALLY_REFUNDED: 'partially_refunded';
297
- };
298
-
299
- export const PAYMENT_GATEWAY_TYPE: {
300
- MANUAL: 'manual';
301
- STRIPE: 'stripe';
302
- SSLCOMMERZ: 'sslcommerz';
303
- };
304
-
305
- export const SUBSCRIPTION_STATUS: {
306
- ACTIVE: 'active';
307
- PAUSED: 'paused';
308
- CANCELLED: 'cancelled';
309
- EXPIRED: 'expired';
310
- PENDING: 'pending';
311
- INACTIVE: 'inactive';
312
- };
313
-
314
- export const PLAN_KEYS: {
315
- MONTHLY: 'monthly';
316
- QUARTERLY: 'quarterly';
317
- YEARLY: 'yearly';
318
- };
319
-
320
- export const MONETIZATION_TYPES: {
321
- FREE: 'free';
322
- PURCHASE: 'purchase';
323
- SUBSCRIPTION: 'subscription';
324
- };
325
-
326
- // ============ SCHEMAS ============
327
-
328
- export const currentPaymentSchema: Schema;
329
- export const paymentSummarySchema: Schema;
330
- export const subscriptionInfoSchema: Schema;
331
- export const subscriptionPlanSchema: Schema;
332
- export const gatewaySchema: Schema;
333
- export const commissionSchema: Schema;
334
- export const paymentDetailsSchema: Schema;
335
-
336
- // ============ UTILITIES ============
337
-
338
- export const logger: Console | any;
339
- export function setLogger(logger: Console | any): void;
340
-
341
- // ============ DEFAULT EXPORT ============
342
-
343
- declare const _default: {
344
- createRevenue: typeof createRevenue;
345
- PaymentProvider: typeof PaymentProvider;
346
- RevenueError: typeof RevenueError;
347
- Container: typeof Container;
348
- };
349
-
350
- export default _default;