@classytic/revenue 0.0.2 → 0.0.21

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 CHANGED
@@ -21,6 +21,51 @@ Thin, focused, production-ready library with smart defaults. Built for SaaS, mar
21
21
  npm install @classytic/revenue
22
22
  ```
23
23
 
24
+ ## Core Concepts
25
+
26
+ ### Monetization Types (Strict)
27
+ The library supports **3 monetization types** (strict):
28
+ - **FREE**: No payment required
29
+ - **SUBSCRIPTION**: Recurring payments
30
+ - **PURCHASE**: One-time payments
31
+
32
+ ### Transaction Categories (Flexible)
33
+ You can use **custom category names** for your business logic while using the strict monetization types:
34
+ - `'order_subscription'` for subscription orders
35
+ - `'gym_membership'` for gym memberships
36
+ - `'course_enrollment'` for course enrollments
37
+ - Or any custom names you need
38
+
39
+ ### How It Works
40
+ ```javascript
41
+ const revenue = createRevenue({
42
+ models: { Transaction },
43
+ config: {
44
+ categoryMappings: {
45
+ Order: 'order_subscription', // Customer orders
46
+ PlatformSubscription: 'platform_subscription', // Tenant/org subscriptions
47
+ Membership: 'gym_membership', // User memberships
48
+ Enrollment: 'course_enrollment', // Course enrollments
49
+ }
50
+ }
51
+ });
52
+
53
+ // All these use SUBSCRIPTION monetization type but different categories
54
+ await revenue.subscriptions.create({
55
+ entity: 'Order', // Logical identifier → maps to 'order_subscription'
56
+ monetizationType: 'subscription',
57
+ // ...
58
+ });
59
+
60
+ await revenue.subscriptions.create({
61
+ entity: 'PlatformSubscription', // Logical identifier → maps to 'platform_subscription'
62
+ monetizationType: 'subscription',
63
+ // ...
64
+ });
65
+ ```
66
+
67
+ **Note:** `entity` is NOT a database model name - it's just a logical identifier you choose to organize your business logic.
68
+
24
69
  ## Transaction Model Setup
25
70
 
26
71
  Spread library enums/schemas into your Transaction model:
@@ -37,9 +82,13 @@ import {
37
82
  paymentDetailsSchema,
38
83
  } from '@classytic/revenue/schemas';
39
84
 
40
- // Merge library categories with your own
85
+ // Merge library categories with your custom ones
41
86
  const MY_CATEGORIES = {
42
- ...LIBRARY_CATEGORIES, // subscription, purchase
87
+ ...LIBRARY_CATEGORIES, // subscription, purchase (library defaults)
88
+ ORDER_SUBSCRIPTION: 'order_subscription',
89
+ ORDER_PURCHASE: 'order_purchase',
90
+ GYM_MEMBERSHIP: 'gym_membership',
91
+ COURSE_ENROLLMENT: 'course_enrollment',
43
92
  SALARY: 'salary',
44
93
  RENT: 'rent',
45
94
  EQUIPMENT: 'equipment',
@@ -91,6 +140,145 @@ const { subscription, transaction } = await revenue.subscriptions.create({
91
140
 
92
141
  That's it! The package works immediately with sensible defaults.
93
142
 
143
+ ## Real-World Use Cases
144
+
145
+ ### E-commerce Platform with Multiple Order Types
146
+
147
+ ```javascript
148
+ // Setup
149
+ const revenue = createRevenue({
150
+ models: { Transaction },
151
+ config: {
152
+ categoryMappings: {
153
+ Order: 'order_subscription', // Recurring orders (meal kits, subscriptions)
154
+ Purchase: 'order_purchase', // One-time orders
155
+ }
156
+ }
157
+ });
158
+
159
+ // Subscription order (meal kit subscription)
160
+ const { subscription, transaction } = await revenue.subscriptions.create({
161
+ data: { organizationId, customerId },
162
+ entity: 'Order', // Logical identifier
163
+ monetizationType: 'subscription', // STRICT: Must be subscription/purchase/free
164
+ planKey: 'monthly',
165
+ amount: 49.99,
166
+ metadata: { productType: 'meal_kit' }
167
+ });
168
+ // Transaction created with category: 'order_subscription'
169
+
170
+ // One-time purchase order
171
+ const { transaction } = await revenue.subscriptions.create({
172
+ data: { organizationId, customerId },
173
+ entity: 'Purchase', // Logical identifier
174
+ monetizationType: 'purchase',
175
+ amount: 99.99,
176
+ metadata: { productType: 'electronics' }
177
+ });
178
+ // Transaction created with category: 'order_purchase'
179
+ ```
180
+
181
+ ### Gym Management System
182
+
183
+ ```javascript
184
+ // Setup
185
+ const revenue = createRevenue({
186
+ models: { Transaction },
187
+ config: {
188
+ categoryMappings: {
189
+ Membership: 'gym_membership',
190
+ PersonalTraining: 'personal_training',
191
+ DayPass: 'day_pass',
192
+ }
193
+ }
194
+ });
195
+
196
+ // Monthly gym membership
197
+ await revenue.subscriptions.create({
198
+ entity: 'Membership',
199
+ monetizationType: 'subscription',
200
+ planKey: 'monthly',
201
+ amount: 59.99,
202
+ });
203
+ // Transaction: 'gym_membership'
204
+
205
+ // Personal training package (one-time purchase)
206
+ await revenue.subscriptions.create({
207
+ entity: 'PersonalTraining',
208
+ monetizationType: 'purchase',
209
+ amount: 299.99,
210
+ });
211
+ // Transaction: 'personal_training'
212
+
213
+ // Day pass (free trial)
214
+ await revenue.subscriptions.create({
215
+ entity: 'DayPass',
216
+ monetizationType: 'free',
217
+ amount: 0,
218
+ });
219
+ // No transaction created for free
220
+ ```
221
+
222
+ ### Online Learning Platform
223
+
224
+ ```javascript
225
+ // Setup
226
+ const revenue = createRevenue({
227
+ models: { Transaction },
228
+ config: {
229
+ categoryMappings: {
230
+ CourseEnrollment: 'course_enrollment',
231
+ MembershipPlan: 'membership_plan',
232
+ }
233
+ }
234
+ });
235
+
236
+ // One-time course purchase
237
+ await revenue.subscriptions.create({
238
+ entity: 'CourseEnrollment',
239
+ monetizationType: 'purchase',
240
+ amount: 99.00,
241
+ metadata: { courseId: 'react-advanced' }
242
+ });
243
+ // Transaction: 'course_enrollment'
244
+
245
+ // Monthly all-access membership
246
+ await revenue.subscriptions.create({
247
+ entity: 'MembershipPlan',
248
+ monetizationType: 'subscription',
249
+ planKey: 'monthly',
250
+ amount: 29.99,
251
+ });
252
+ // Transaction: 'membership_plan'
253
+ ```
254
+
255
+ ### Without Category Mappings (Defaults)
256
+
257
+ ```javascript
258
+ // No mappings defined - uses library defaults
259
+ const revenue = createRevenue({
260
+ models: { Transaction },
261
+ config: {
262
+ categoryMappings: {} // Empty or omit this
263
+ }
264
+ });
265
+
266
+ // All subscriptions use default 'subscription' category
267
+ await revenue.subscriptions.create({
268
+ monetizationType: 'subscription',
269
+ planKey: 'monthly',
270
+ amount: 49.99,
271
+ });
272
+ // Transaction created with category: 'subscription' (library default)
273
+
274
+ // All purchases use default 'purchase' category
275
+ await revenue.subscriptions.create({
276
+ monetizationType: 'purchase',
277
+ amount: 99.99,
278
+ });
279
+ // Transaction created with category: 'purchase' (library default)
280
+ ```
281
+
94
282
  ## Usage Examples
95
283
 
96
284
  ### With Payment Provider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/revenue",
3
- "version": "0.0.2",
3
+ "version": "0.0.21",
4
4
  "description": "Enterprise revenue management system with subscriptions, purchases, proration, and payment processing",
5
5
  "main": "index.js",
6
6
  "types": "revenue.d.ts",
package/revenue.d.ts CHANGED
@@ -105,13 +105,21 @@ export class SubscriptionService {
105
105
  amount: number;
106
106
  currency?: string;
107
107
  gateway?: string;
108
+ entity?: string;
109
+ monetizationType?: 'free' | 'subscription' | 'purchase';
108
110
  paymentData?: any;
109
111
  metadata?: Record<string, any>;
110
112
  idempotencyKey?: string;
111
113
  }): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent | null }>;
112
114
 
113
115
  activate(subscriptionId: string, options?: { timestamp?: Date }): Promise<any>;
114
- renew(subscriptionId: string, params?: any): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent }>;
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 }>;
115
123
  cancel(subscriptionId: string, options?: { immediate?: boolean; reason?: string }): Promise<any>;
116
124
  pause(subscriptionId: string, options?: { reason?: string }): Promise<any>;
117
125
  resume(subscriptionId: string, options?: { extendPeriod?: boolean }): Promise<any>;
@@ -193,7 +201,23 @@ export interface RevenueOptions {
193
201
  providers?: Record<string, PaymentProvider>;
194
202
  hooks?: Record<string, Function[]>;
195
203
  config?: {
196
- targetModels?: string[];
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
+ */
197
221
  categoryMappings?: Record<string, string>;
198
222
  [key: string]: any;
199
223
  };
@@ -218,19 +242,10 @@ export const TRANSACTION_STATUS: {
218
242
  PARTIALLY_REFUNDED: 'partially_refunded';
219
243
  };
220
244
 
221
- export const TRANSACTION_TYPES: {
222
- INCOME: 'income';
223
- EXPENSE: 'expense';
224
- };
225
-
226
- // Note: PAYMENT_METHOD removed - users define their own payment methods
227
-
228
245
  export const PAYMENT_GATEWAY_TYPE: {
229
246
  MANUAL: 'manual';
230
247
  STRIPE: 'stripe';
231
248
  SSLCOMMERZ: 'sslcommerz';
232
- BKASH_GATEWAY: 'bkash_gateway';
233
- NAGAD_GATEWAY: 'nagad_gateway';
234
249
  };
235
250
 
236
251
  export const SUBSCRIPTION_STATUS: {
@@ -263,15 +278,6 @@ export const subscriptionPlanSchema: Schema;
263
278
  export const gatewaySchema: Schema;
264
279
  export const commissionSchema: Schema;
265
280
  export const paymentDetailsSchema: Schema;
266
- export const tenantSnapshotSchema: Schema;
267
- export const timelineEventSchema: Schema;
268
- export const customerInfoSchema: Schema;
269
- export const customDiscountSchema: Schema;
270
- export const stripeAccountSchema: Schema;
271
- export const sslcommerzAccountSchema: Schema;
272
- export const bkashMerchantSchema: Schema;
273
- export const bankAccountSchema: Schema;
274
- export const walletSchema: Schema;
275
281
 
276
282
  // ============ UTILITIES ============
277
283
 
@@ -12,23 +12,11 @@ export const paymentSummarySchema: Schema;
12
12
  export const paymentDetailsSchema: Schema;
13
13
  export const gatewaySchema: Schema;
14
14
  export const commissionSchema: Schema;
15
- export const tenantSnapshotSchema: Schema;
16
- export const timelineEventSchema: Schema;
17
- export const customerInfoSchema: Schema;
18
15
 
19
16
  // ============ SUBSCRIPTION SCHEMAS ============
20
17
 
21
18
  export const subscriptionInfoSchema: Schema;
22
19
  export const subscriptionPlanSchema: Schema;
23
- export const customDiscountSchema: Schema;
24
-
25
- // ============ GATEWAY ACCOUNT SCHEMAS ============
26
-
27
- export const stripeAccountSchema: Schema;
28
- export const sslcommerzAccountSchema: Schema;
29
- export const bkashMerchantSchema: Schema;
30
- export const bankAccountSchema: Schema;
31
- export const walletSchema: Schema;
32
20
 
33
21
  // ============ DEFAULT EXPORT ============
34
22
 
@@ -38,17 +26,8 @@ declare const _default: {
38
26
  paymentDetailsSchema: Schema;
39
27
  gatewaySchema: Schema;
40
28
  commissionSchema: Schema;
41
- tenantSnapshotSchema: Schema;
42
- timelineEventSchema: Schema;
43
- customerInfoSchema: Schema;
44
29
  subscriptionInfoSchema: Schema;
45
30
  subscriptionPlanSchema: Schema;
46
- customDiscountSchema: Schema;
47
- stripeAccountSchema: Schema;
48
- sslcommerzAccountSchema: Schema;
49
- bkashMerchantSchema: Schema;
50
- bankAccountSchema: Schema;
51
- walletSchema: Schema;
52
31
  };
53
32
 
54
33
  export default _default;
@@ -132,7 +132,7 @@ export class PaymentService {
132
132
  }
133
133
 
134
134
  if (!transaction) {
135
- throw new Error('Transaction not found');
135
+ throw new TransactionNotFoundError(paymentIntentId);
136
136
  }
137
137
 
138
138
  // Get provider
@@ -140,7 +140,7 @@ export class PaymentService {
140
140
  const provider = this.providers[gatewayType];
141
141
 
142
142
  if (!provider) {
143
- throw new Error(`Payment provider "${gatewayType}" not found`);
143
+ throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
144
144
  }
145
145
 
146
146
  // Get status from provider
@@ -218,7 +218,7 @@ export class PaymentService {
218
218
  refundResult = await provider.refund(paymentId, refundAmount, { reason });
219
219
  } catch (error) {
220
220
  this.logger.error('Refund failed:', error);
221
- throw new Error(`Refund failed: ${error.message}`);
221
+ throw new RefundError(paymentId, error.message);
222
222
  }
223
223
 
224
224
  // Update transaction
@@ -262,7 +262,7 @@ export class PaymentService {
262
262
  const provider = this.providers[providerName];
263
263
 
264
264
  if (!provider) {
265
- throw new Error(`Payment provider "${providerName}" not found`);
265
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
266
266
  }
267
267
 
268
268
  // Process webhook via provider
@@ -271,7 +271,7 @@ export class PaymentService {
271
271
  webhookEvent = await provider.handleWebhook(payload, headers);
272
272
  } catch (error) {
273
273
  this.logger.error('Webhook processing failed:', error);
274
- throw new Error(`Webhook processing failed: ${error.message}`);
274
+ throw new ProviderError(providerName, `Webhook processing failed: ${error.message}`);
275
275
  }
276
276
 
277
277
  // Find transaction by payment intent ID from webhook
@@ -286,7 +286,7 @@ export class PaymentService {
286
286
  eventId: webhookEvent.id,
287
287
  paymentIntentId: webhookEvent.data.paymentIntentId,
288
288
  });
289
- throw new Error('Transaction not found for payment intent');
289
+ throw new TransactionNotFoundError(webhookEvent.data.paymentIntentId);
290
290
  }
291
291
 
292
292
  // Check for duplicate webhook processing (idempotency)
@@ -368,7 +368,7 @@ export class PaymentService {
368
368
  const transaction = await TransactionModel.findById(transactionId);
369
369
 
370
370
  if (!transaction) {
371
- throw new Error('Transaction not found');
371
+ throw new TransactionNotFoundError(transactionId);
372
372
  }
373
373
 
374
374
  return transaction;
@@ -383,7 +383,7 @@ export class PaymentService {
383
383
  getProvider(providerName) {
384
384
  const provider = this.providers[providerName];
385
385
  if (!provider) {
386
- throw new Error(`Payment provider "${providerName}" not found. Available: ${Object.keys(this.providers).join(', ')}`);
386
+ throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
387
387
  }
388
388
  return provider;
389
389
  }
@@ -17,6 +17,8 @@ import {
17
17
  PaymentIntentCreationError,
18
18
  } from '../core/errors.js';
19
19
  import { triggerHook } from '../utils/hooks.js';
20
+ import { resolveCategory } from '../utils/category-resolver.js';
21
+ import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
20
22
 
21
23
  /**
22
24
  * Subscription Service
@@ -41,6 +43,9 @@ export class SubscriptionService {
41
43
  * @param {Number} params.amount - Subscription amount
42
44
  * @param {String} params.currency - Currency code (default: 'BDT')
43
45
  * @param {String} params.gateway - Payment gateway to use (default: 'manual')
46
+ * @param {String} params.entity - Logical entity identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
47
+ * NOTE: This is NOT a database model name - it's just a logical identifier for categoryMappings
48
+ * @param {String} params.monetizationType - Monetization type ('free', 'subscription', 'purchase')
44
49
  * @param {Object} params.paymentData - Payment method details
45
50
  * @param {Object} params.metadata - Additional metadata
46
51
  * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
@@ -53,6 +58,8 @@ export class SubscriptionService {
53
58
  amount,
54
59
  currency = 'BDT',
55
60
  gateway = 'manual',
61
+ entity = null,
62
+ monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
56
63
  paymentData,
57
64
  metadata = {},
58
65
  idempotencyKey = null,
@@ -99,6 +106,9 @@ export class SubscriptionService {
99
106
  throw new PaymentIntentCreationError(gateway, error);
100
107
  }
101
108
 
109
+ // Resolve category based on entity and monetizationType
110
+ const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
111
+
102
112
  // Create transaction record
103
113
  const TransactionModel = this.models.Transaction;
104
114
  transaction = await TransactionModel.create({
@@ -106,7 +116,7 @@ export class SubscriptionService {
106
116
  customerId: data.customerId || null,
107
117
  amount,
108
118
  currency,
109
- category: 'platform_subscription',
119
+ category,
110
120
  type: 'credit',
111
121
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
112
122
  gateway: {
@@ -121,6 +131,8 @@ export class SubscriptionService {
121
131
  metadata: {
122
132
  ...metadata,
123
133
  planKey,
134
+ entity,
135
+ monetizationType,
124
136
  paymentIntentId: paymentIntent.id,
125
137
  },
126
138
  idempotencyKey: idempotencyKey || `sub_${nanoid(16)}`,
@@ -146,6 +158,8 @@ export class SubscriptionService {
146
158
  metadata: {
147
159
  ...metadata,
148
160
  isFree,
161
+ entity,
162
+ monetizationType,
149
163
  },
150
164
  ...data,
151
165
  });
@@ -218,35 +232,41 @@ export class SubscriptionService {
218
232
  *
219
233
  * @param {String} subscriptionId - Subscription ID
220
234
  * @param {Object} params - Renewal parameters
235
+ * @param {String} params.gateway - Payment gateway to use (default: 'manual')
236
+ * @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
237
+ * @param {Object} params.paymentData - Payment method details
238
+ * @param {Object} params.metadata - Additional metadata
239
+ * @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
221
240
  * @returns {Promise<Object>} { subscription, transaction, paymentIntent }
222
241
  */
223
242
  async renew(subscriptionId, params = {}) {
224
243
  const {
225
244
  gateway = 'manual',
245
+ entity = null,
226
246
  paymentData,
227
247
  metadata = {},
228
248
  idempotencyKey = null,
229
249
  } = params;
230
250
 
231
251
  if (!this.models.Subscription) {
232
- throw new Error('Subscription model not registered');
252
+ throw new ModelNotRegisteredError('Subscription');
233
253
  }
234
254
 
235
255
  const SubscriptionModel = this.models.Subscription;
236
256
  const subscription = await SubscriptionModel.findById(subscriptionId);
237
257
 
238
258
  if (!subscription) {
239
- throw new Error('Subscription not found');
259
+ throw new SubscriptionNotFoundError(subscriptionId);
240
260
  }
241
261
 
242
262
  if (subscription.amount === 0) {
243
- throw new Error('Free subscriptions do not require renewal');
263
+ throw new InvalidAmountError(0, 'Free subscriptions do not require renewal');
244
264
  }
245
265
 
246
266
  // Get provider
247
267
  const provider = this.providers[gateway];
248
268
  if (!provider) {
249
- throw new Error(`Payment provider "${gateway}" not found`);
269
+ throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
250
270
  }
251
271
 
252
272
  // Create payment intent
@@ -260,6 +280,11 @@ export class SubscriptionService {
260
280
  },
261
281
  });
262
282
 
283
+ // Resolve category - use provided entity or inherit from subscription metadata
284
+ const effectiveEntity = entity || subscription.metadata?.entity;
285
+ const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
286
+ const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
287
+
263
288
  // Create transaction
264
289
  const TransactionModel = this.models.Transaction;
265
290
  const transaction = await TransactionModel.create({
@@ -267,7 +292,7 @@ export class SubscriptionService {
267
292
  customerId: subscription.customerId,
268
293
  amount: subscription.amount,
269
294
  currency: subscription.currency || 'BDT',
270
- category: 'platform_subscription',
295
+ category,
271
296
  type: 'credit',
272
297
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
273
298
  gateway: {
@@ -282,6 +307,8 @@ export class SubscriptionService {
282
307
  metadata: {
283
308
  ...metadata,
284
309
  subscriptionId: subscription._id.toString(),
310
+ entity: effectiveEntity,
311
+ monetizationType: effectiveMonetizationType,
285
312
  isRenewal: true,
286
313
  paymentIntentId: paymentIntent.id,
287
314
  },
@@ -322,14 +349,14 @@ export class SubscriptionService {
322
349
  const { immediate = false, reason = null } = options;
323
350
 
324
351
  if (!this.models.Subscription) {
325
- throw new Error('Subscription model not registered');
352
+ throw new ModelNotRegisteredError('Subscription');
326
353
  }
327
354
 
328
355
  const SubscriptionModel = this.models.Subscription;
329
356
  const subscription = await SubscriptionModel.findById(subscriptionId);
330
357
 
331
358
  if (!subscription) {
332
- throw new Error('Subscription not found');
359
+ throw new SubscriptionNotFoundError(subscriptionId);
333
360
  }
334
361
 
335
362
  const now = new Date();
@@ -369,18 +396,18 @@ export class SubscriptionService {
369
396
  const { reason = null } = options;
370
397
 
371
398
  if (!this.models.Subscription) {
372
- throw new Error('Subscription model not registered');
399
+ throw new ModelNotRegisteredError('Subscription');
373
400
  }
374
401
 
375
402
  const SubscriptionModel = this.models.Subscription;
376
403
  const subscription = await SubscriptionModel.findById(subscriptionId);
377
404
 
378
405
  if (!subscription) {
379
- throw new Error('Subscription not found');
406
+ throw new SubscriptionNotFoundError(subscriptionId);
380
407
  }
381
408
 
382
409
  if (!subscription.isActive) {
383
- throw new Error('Only active subscriptions can be paused');
410
+ throw new SubscriptionNotActiveError(subscriptionId, 'Only active subscriptions can be paused');
384
411
  }
385
412
 
386
413
  const pausedAt = new Date();
@@ -412,18 +439,23 @@ export class SubscriptionService {
412
439
  const { extendPeriod = false } = options;
413
440
 
414
441
  if (!this.models.Subscription) {
415
- throw new Error('Subscription model not registered');
442
+ throw new ModelNotRegisteredError('Subscription');
416
443
  }
417
444
 
418
445
  const SubscriptionModel = this.models.Subscription;
419
446
  const subscription = await SubscriptionModel.findById(subscriptionId);
420
447
 
421
448
  if (!subscription) {
422
- throw new Error('Subscription not found');
449
+ throw new SubscriptionNotFoundError(subscriptionId);
423
450
  }
424
451
 
425
452
  if (!subscription.pausedAt) {
426
- throw new Error('Only paused subscriptions can be resumed');
453
+ throw new InvalidStateTransitionError(
454
+ 'resume',
455
+ 'paused',
456
+ subscription.status,
457
+ 'Only paused subscriptions can be resumed'
458
+ );
427
459
  }
428
460
 
429
461
  const now = new Date();
@@ -463,7 +495,7 @@ export class SubscriptionService {
463
495
  */
464
496
  async list(filters = {}, options = {}) {
465
497
  if (!this.models.Subscription) {
466
- throw new Error('Subscription model not registered');
498
+ throw new ModelNotRegisteredError('Subscription');
467
499
  }
468
500
 
469
501
  const SubscriptionModel = this.models.Subscription;
@@ -486,14 +518,14 @@ export class SubscriptionService {
486
518
  */
487
519
  async get(subscriptionId) {
488
520
  if (!this.models.Subscription) {
489
- throw new Error('Subscription model not registered');
521
+ throw new ModelNotRegisteredError('Subscription');
490
522
  }
491
523
 
492
524
  const SubscriptionModel = this.models.Subscription;
493
525
  const subscription = await SubscriptionModel.findById(subscriptionId);
494
526
 
495
527
  if (!subscription) {
496
- throw new Error('Subscription not found');
528
+ throw new SubscriptionNotFoundError(subscriptionId);
497
529
  }
498
530
 
499
531
  return subscription;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Category Resolver Utility
3
+ * @classytic/revenue
4
+ *
5
+ * Resolves transaction category based on referenceModel and categoryMappings
6
+ */
7
+
8
+ import { LIBRARY_CATEGORIES } from '../enums/transaction.enums.js';
9
+
10
+ /**
11
+ * Resolve category for a transaction based on entity and monetizationType
12
+ *
13
+ * Resolution Logic:
14
+ * 1. If categoryMappings[entity] exists → use it
15
+ * 2. Otherwise → fall back to default library category
16
+ *
17
+ * @param {String} entity - The logical entity/identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
18
+ * NOTE: This is NOT a database model name - it's just a logical identifier
19
+ * @param {String} monetizationType - The monetization type ('subscription', 'purchase', 'free')
20
+ * @param {Object} categoryMappings - User-defined category mappings from config
21
+ * @returns {String} Category name for the transaction
22
+ *
23
+ * @example
24
+ * // With mapping defined
25
+ * resolveCategory('Order', 'subscription', { Order: 'order_subscription' })
26
+ * // Returns: 'order_subscription'
27
+ *
28
+ * @example
29
+ * // Without mapping, falls back to library default
30
+ * resolveCategory('Order', 'subscription', {})
31
+ * // Returns: 'subscription'
32
+ *
33
+ * @example
34
+ * // Different entities with different mappings
35
+ * const mappings = {
36
+ * Order: 'order_subscription',
37
+ * PlatformSubscription: 'platform_subscription',
38
+ * TenantUpgrade: 'tenant_upgrade',
39
+ * Membership: 'gym_membership',
40
+ * Enrollment: 'course_enrollment',
41
+ * };
42
+ * resolveCategory('PlatformSubscription', 'subscription', mappings)
43
+ * // Returns: 'platform_subscription'
44
+ */
45
+ export function resolveCategory(entity, monetizationType, categoryMappings = {}) {
46
+ // If user has defined a custom mapping for this entity, use it
47
+ if (entity && categoryMappings[entity]) {
48
+ return categoryMappings[entity];
49
+ }
50
+
51
+ // Otherwise, fall back to library default based on monetization type
52
+ switch (monetizationType) {
53
+ case 'subscription':
54
+ return LIBRARY_CATEGORIES.SUBSCRIPTION; // 'subscription'
55
+ case 'purchase':
56
+ return LIBRARY_CATEGORIES.PURCHASE; // 'purchase'
57
+ default:
58
+ return LIBRARY_CATEGORIES.SUBSCRIPTION; // Default to subscription
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Validate that a category is defined in user's Transaction model enum
64
+ * This is informational - actual validation happens at Mongoose schema level
65
+ *
66
+ * @param {String} category - Category to validate
67
+ * @param {Array<String>} allowedCategories - List of allowed categories
68
+ * @returns {Boolean} Whether category is valid
69
+ */
70
+ export function isCategoryValid(category, allowedCategories = []) {
71
+ return allowedCategories.includes(category);
72
+ }
73
+
74
+ export default resolveCategory;
package/utils/logger.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Usage:
8
8
  * ```javascript
9
- * import { setLogger } from '@fitverse/monetization';
9
+ * import { setLogger } from '@classytic/revenue';
10
10
  *
11
11
  * // Optional: Use your own logger
12
12
  * setLogger(myPinoLogger);
@@ -1,171 +0,0 @@
1
- /**
2
- * Manual Payment Provider
3
- * @classytic/revenue
4
- *
5
- * Built-in provider for manual payment verification
6
- * Perfect for: Cash, bank transfers, mobile money without API
7
- */
8
-
9
- import { PaymentProvider, PaymentIntent, PaymentResult, RefundResult } from './base.js';
10
- import { nanoid } from 'nanoid';
11
-
12
- export class ManualProvider extends PaymentProvider {
13
- constructor(config = {}) {
14
- super(config);
15
- this.name = 'manual';
16
- }
17
-
18
- /**
19
- * Create manual payment intent
20
- * Returns instructions for manual payment
21
- */
22
- async createIntent(params) {
23
- const intentId = `manual_${nanoid(16)}`;
24
-
25
- return new PaymentIntent({
26
- id: intentId,
27
- provider: 'manual',
28
- status: 'pending',
29
- amount: params.amount,
30
- currency: params.currency || 'BDT',
31
- metadata: params.metadata || {},
32
- instructions: this._getPaymentInstructions(params),
33
- raw: params,
34
- });
35
- }
36
-
37
- /**
38
- * Verify manual payment
39
- * Note: This is called by admin after checking payment proof
40
- */
41
- async verifyPayment(intentId) {
42
- // Manual verification doesn't auto-verify
43
- // Admin must explicitly call payment verification endpoint
44
- return new PaymentResult({
45
- id: intentId,
46
- provider: 'manual',
47
- status: 'requires_manual_approval',
48
- amount: 0, // Amount will be filled by transaction
49
- currency: 'BDT',
50
- metadata: {
51
- message: 'Manual payment requires admin verification',
52
- },
53
- });
54
- }
55
-
56
- /**
57
- * Get payment status
58
- */
59
- async getStatus(intentId) {
60
- return this.verifyPayment(intentId);
61
- }
62
-
63
- /**
64
- * Refund manual payment
65
- */
66
- async refund(paymentId, amount, options = {}) {
67
- const refundId = `refund_${nanoid(16)}`;
68
-
69
- return new RefundResult({
70
- id: refundId,
71
- provider: 'manual',
72
- status: 'succeeded', // Manual refunds are immediately marked as succeeded
73
- amount: amount,
74
- currency: options.currency || 'BDT',
75
- refundedAt: new Date(),
76
- reason: options.reason || 'Manual refund',
77
- metadata: options.metadata || {},
78
- });
79
- }
80
-
81
- /**
82
- * Manual provider doesn't support webhooks
83
- */
84
- async handleWebhook(payload, headers) {
85
- throw new Error('Manual provider does not support webhooks');
86
- }
87
-
88
- /**
89
- * Get provider capabilities
90
- */
91
- getCapabilities() {
92
- return {
93
- supportsWebhooks: false,
94
- supportsRefunds: true,
95
- supportsPartialRefunds: true,
96
- requiresManualVerification: true,
97
- supportedMethods: ['cash', 'bank', 'bkash', 'nagad', 'rocket', 'manual'],
98
- };
99
- }
100
-
101
- /**
102
- * Generate payment instructions for customer
103
- * @private
104
- */
105
- _getPaymentInstructions(params) {
106
- const { organizationPaymentInfo, method } = params.metadata || {};
107
-
108
- if (!organizationPaymentInfo) {
109
- return 'Please contact the organization for payment details.';
110
- }
111
-
112
- const instructions = [];
113
-
114
- // Add method-specific instructions
115
- switch (method) {
116
- case 'bkash':
117
- case 'nagad':
118
- case 'rocket':
119
- if (organizationPaymentInfo[`${method}Number`]) {
120
- instructions.push(
121
- `Send money via ${method.toUpperCase()}:`,
122
- `Number: ${organizationPaymentInfo[`${method}Number`]}`,
123
- `Amount: ${params.amount} ${params.currency || 'BDT'}`,
124
- ``,
125
- `After payment, provide the transaction ID/reference number.`
126
- );
127
- }
128
- break;
129
-
130
- case 'bank':
131
- if (organizationPaymentInfo.bankAccount) {
132
- const bank = organizationPaymentInfo.bankAccount;
133
- instructions.push(
134
- `Bank Transfer Details:`,
135
- `Bank: ${bank.bankName || 'N/A'}`,
136
- `Account: ${bank.accountNumber || 'N/A'}`,
137
- `Account Name: ${bank.accountName || 'N/A'}`,
138
- `Amount: ${params.amount} ${params.currency || 'BDT'}`,
139
- ``,
140
- `After payment, upload proof and provide reference.`
141
- );
142
- }
143
- break;
144
-
145
- case 'cash':
146
- instructions.push(
147
- `Cash Payment:`,
148
- `Amount: ${params.amount} ${params.currency || 'BDT'}`,
149
- ``,
150
- `Pay at the organization's office and get a receipt.`
151
- );
152
- break;
153
-
154
- default:
155
- instructions.push(
156
- `Payment Amount: ${params.amount} ${params.currency || 'BDT'}`,
157
- ``,
158
- `Contact the organization for payment details.`
159
- );
160
- }
161
-
162
- // Add custom instructions if provided
163
- if (organizationPaymentInfo.paymentInstructions) {
164
- instructions.push(``, `Additional Instructions:`, organizationPaymentInfo.paymentInstructions);
165
- }
166
-
167
- return instructions.join('\n');
168
- }
169
- }
170
-
171
- export default ManualProvider;