@classytic/revenue 0.0.21 → 0.0.22

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
@@ -6,637 +6,375 @@ Thin, focused, production-ready library with smart defaults. Built for SaaS, mar
6
6
 
7
7
  ## Features
8
8
 
9
- - **Subscriptions**: Create, renew, upgrade, downgrade with smart proration
10
- - **Payment Processing**: Multi-gateway support (Stripe, SSLCommerz, bKash, manual)
11
- - **Transaction Management**: Complete lifecycle with verification and refunds
12
- - **Provider Pattern**: Pluggable payment providers (like AI SDK)
13
- - **Framework Agnostic**: Works with Fastify, Express, Nest, or standalone
14
- - **Model Flexible**: Plain Mongoose OR @classytic/mongokit Repository
9
+ - **Subscriptions**: Create, renew, pause, cancel with lifecycle management
10
+ - **Payment Processing**: Multi-gateway support (Stripe, SSLCommerz, manual, etc.)
11
+ - **Transaction Management**: Income/expense tracking with verification and refunds
12
+ - **Provider Pattern**: Pluggable payment providers (like LangChain/Vercel AI SDK)
13
+ - **Framework Agnostic**: Works with Express, Fastify, Next.js, or standalone
15
14
  - **TypeScript Ready**: Full type definitions included
16
- - **Zero Dependencies**: Only requires `mongoose` as peer dependency
17
15
 
18
16
  ## Installation
19
17
 
20
18
  ```bash
21
19
  npm install @classytic/revenue
20
+ npm install @classytic/revenue-manual # For manual payments
22
21
  ```
23
22
 
24
- ## Core Concepts
23
+ ## Quick Start (30 seconds)
25
24
 
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
25
  ```javascript
26
+ import { createRevenue } from '@classytic/revenue';
27
+ import { ManualProvider } from '@classytic/revenue-manual';
28
+ import Transaction from './models/Transaction.js';
29
+
30
+ // 1. Configure
41
31
  const revenue = createRevenue({
42
32
  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
- }
33
+ providers: { manual: new ManualProvider() },
51
34
  });
52
35
 
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
- // ...
36
+ // 2. Create subscription
37
+ const { subscription, transaction } = await revenue.subscriptions.create({
38
+ data: { organizationId, customerId },
39
+ planKey: 'monthly',
40
+ amount: 1500,
41
+ gateway: 'manual',
42
+ paymentData: { method: 'bkash', walletNumber: '01712345678' },
58
43
  });
59
44
 
60
- await revenue.subscriptions.create({
61
- entity: 'PlatformSubscription', // Logical identifier → maps to 'platform_subscription'
62
- monetizationType: 'subscription',
63
- // ...
64
- });
45
+ // 3. Verify payment
46
+ await revenue.payments.verify(transaction.gateway.paymentIntentId);
47
+
48
+ // 4. Refund if needed
49
+ await revenue.payments.refund(transaction._id, 500, { reason: 'Partial refund' });
65
50
  ```
66
51
 
67
- **Note:** `entity` is NOT a database model name - it's just a logical identifier you choose to organize your business logic.
52
+ **That's it!** Working revenue system in 3 steps.
68
53
 
69
54
  ## Transaction Model Setup
70
55
 
71
- Spread library enums/schemas into your Transaction model:
56
+ The library requires a Transaction model with specific fields and provides reusable schemas:
72
57
 
73
58
  ```javascript
74
59
  import mongoose from 'mongoose';
75
60
  import {
61
+ TRANSACTION_TYPE_VALUES,
76
62
  TRANSACTION_STATUS_VALUES,
77
- LIBRARY_CATEGORIES,
78
63
  } from '@classytic/revenue/enums';
79
64
  import {
80
65
  gatewaySchema,
81
- currentPaymentSchema,
82
66
  paymentDetailsSchema,
83
67
  } from '@classytic/revenue/schemas';
84
68
 
85
- // Merge library categories with your custom ones
86
- const MY_CATEGORIES = {
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',
92
- SALARY: 'salary',
93
- RENT: 'rent',
94
- EQUIPMENT: 'equipment',
95
- // Add as many as you need
96
- };
97
-
98
69
  const transactionSchema = new mongoose.Schema({
99
- // Required by library
70
+ // ============ REQUIRED BY LIBRARY ============
100
71
  organizationId: { type: String, required: true, index: true },
101
72
  amount: { type: Number, required: true, min: 0 },
73
+ type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true }, // 'income' | 'expense'
74
+ method: { type: String, required: true }, // 'manual' | 'bkash' | 'card' | etc.
102
75
  status: { type: String, enum: TRANSACTION_STATUS_VALUES, required: true },
103
- category: { type: String, enum: Object.values(MY_CATEGORIES), required: true },
104
-
105
- // Spread library schemas
106
- gateway: gatewaySchema,
107
- currentPayment: currentPaymentSchema,
108
- paymentDetails: paymentDetailsSchema,
109
-
110
- // Add your fields
111
- notes: String,
112
- invoiceNumber: String,
76
+ category: { type: String, required: true }, // Your custom categories
77
+
78
+ // ============ LIBRARY SCHEMAS (nested) ============
79
+ gateway: gatewaySchema, // Payment gateway details
80
+ paymentDetails: paymentDetailsSchema, // Payment info (wallet, bank, etc.)
81
+
82
+ // ============ YOUR CUSTOM FIELDS ============
83
+ customerId: String,
84
+ currency: { type: String, default: 'BDT' },
85
+ verifiedAt: Date,
86
+ verifiedBy: mongoose.Schema.Types.ObjectId,
87
+ refundedAmount: Number,
88
+ idempotencyKey: { type: String, unique: true, sparse: true },
89
+ metadata: mongoose.Schema.Types.Mixed,
113
90
  }, { timestamps: true });
114
91
 
115
92
  export default mongoose.model('Transaction', transactionSchema);
116
93
  ```
117
94
 
118
- **See [`examples/transaction.model.js`](examples/transaction.model.js) for complete example with indexes.**
95
+ ## Available Schemas
119
96
 
120
- ## Quick Start
97
+ | Schema | Purpose | Key Fields |
98
+ |--------|---------|------------|
99
+ | `gatewaySchema` | Payment gateway integration | `type`, `paymentIntentId`, `sessionId` |
100
+ | `paymentDetailsSchema` | Payment method info | `walletNumber`, `trxId`, `bankName` |
101
+ | `commissionSchema` | Commission tracking | `rate`, `grossAmount`, `netAmount` |
102
+ | `currentPaymentSchema` | Latest payment (for Order/Subscription models) | `transactionId`, `status`, `verifiedAt` |
103
+ | `subscriptionInfoSchema` | Subscription details (for Order models) | `planKey`, `startDate`, `endDate` |
121
104
 
122
- ### Minimal Setup (3 lines)
105
+ **Usage:** Import and use as nested objects (NOT spread):
123
106
 
124
107
  ```javascript
125
- import { createRevenue } from '@classytic/revenue';
126
- import Transaction from './models/Transaction.js';
127
-
128
- // Works out-of-box with built-in manual provider
129
- const revenue = createRevenue({
130
- models: { Transaction },
131
- });
108
+ import { gatewaySchema } from '@classytic/revenue/schemas';
132
109
 
133
- // Create a subscription
134
- const { subscription, transaction } = await revenue.subscriptions.create({
135
- data: { organizationId, customerId },
136
- planKey: 'monthly',
137
- amount: 99.99,
110
+ const schema = new mongoose.Schema({
111
+ gateway: gatewaySchema, // Correct - nested
112
+ // ...gatewaySchema, // Wrong - don't spread
138
113
  });
139
114
  ```
140
115
 
141
- That's it! The package works immediately with sensible defaults.
142
-
143
- ## Real-World Use Cases
116
+ ## Core API
144
117
 
145
- ### E-commerce Platform with Multiple Order Types
118
+ ### Subscriptions
146
119
 
147
120
  ```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
- });
121
+ // Create subscription
122
+ const { subscription, transaction, paymentIntent } =
123
+ await revenue.subscriptions.create({
124
+ data: { organizationId, customerId },
125
+ planKey: 'monthly',
126
+ amount: 1500,
127
+ currency: 'BDT',
128
+ gateway: 'manual',
129
+ paymentData: { method: 'bkash', walletNumber: '01712345678' },
130
+ });
131
+
132
+ // Verify and activate
133
+ await revenue.payments.verify(transaction.gateway.paymentIntentId);
134
+ await revenue.subscriptions.activate(subscription._id);
158
135
 
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' }
136
+ // Renew subscription
137
+ await revenue.subscriptions.renew(subscription._id, {
138
+ gateway: 'manual',
139
+ paymentData: { method: 'nagad' },
167
140
  });
168
- // Transaction created with category: 'order_subscription'
169
141
 
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'
142
+ // Pause/Resume
143
+ await revenue.subscriptions.pause(subscription._id, { reason: 'Customer request' });
144
+ await revenue.subscriptions.resume(subscription._id, { extendPeriod: true });
145
+
146
+ // Cancel
147
+ await revenue.subscriptions.cancel(subscription._id, { immediate: true });
179
148
  ```
180
149
 
181
- ### Gym Management System
150
+ ### Payments
182
151
 
183
152
  ```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
- }
153
+ // Verify payment (admin approval for manual)
154
+ const { transaction } = await revenue.payments.verify(paymentIntentId, {
155
+ verifiedBy: adminUserId,
194
156
  });
195
157
 
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'
158
+ // Get payment status
159
+ const { status } = await revenue.payments.getStatus(paymentIntentId);
204
160
 
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'
161
+ // Refund (creates separate EXPENSE transaction)
162
+ const { transaction, refundTransaction } = await revenue.payments.refund(
163
+ transactionId,
164
+ 500, // Amount or null for full refund
165
+ { reason: 'Customer requested' }
166
+ );
212
167
 
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
168
+ // Handle webhook (for automated providers like Stripe)
169
+ const { event, transaction } = await revenue.payments.handleWebhook(
170
+ 'stripe',
171
+ webhookPayload,
172
+ headers
173
+ );
220
174
  ```
221
175
 
222
- ### Online Learning Platform
176
+ ### Transactions
223
177
 
224
178
  ```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
- });
179
+ // Get transaction by ID
180
+ const transaction = await revenue.transactions.get(transactionId);
235
181
 
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'
182
+ // List with filters
183
+ const { transactions, total } = await revenue.transactions.list(
184
+ { type: 'income', status: 'verified' },
185
+ { limit: 50, sort: { createdAt: -1 } }
186
+ );
244
187
 
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'
188
+ // Calculate net revenue
189
+ const income = await revenue.transactions.list({ type: 'income' });
190
+ const expense = await revenue.transactions.list({ type: 'expense' });
191
+ const netRevenue = income.total - expense.total;
253
192
  ```
254
193
 
255
- ### Without Category Mappings (Defaults)
194
+ ## Transaction Types (Income vs Expense)
195
+
196
+ The library uses **double-entry accounting**:
197
+
198
+ - **INCOME** (`'income'`): Money coming in - payments, subscriptions
199
+ - **EXPENSE** (`'expense'`): Money going out - refunds, payouts
256
200
 
257
201
  ```javascript
258
- // No mappings defined - uses library defaults
259
202
  const revenue = createRevenue({
260
203
  models: { Transaction },
261
204
  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,
205
+ transactionTypeMapping: {
206
+ subscription: 'income',
207
+ purchase: 'income',
208
+ refund: 'expense', // Refunds create separate expense transactions
209
+ },
210
+ },
278
211
  });
279
- // Transaction created with category: 'purchase' (library default)
280
212
  ```
281
213
 
282
- ## Usage Examples
214
+ **Refund Pattern:**
215
+ - Refund creates NEW transaction with `type: 'expense'`
216
+ - Original transaction status becomes `'refunded'` or `'partially_refunded'`
217
+ - Both linked via metadata for audit trail
218
+ - Calculate net: `SUM(income) - SUM(expense)`
283
219
 
284
- ### With Payment Provider
220
+ ## Custom Categories
285
221
 
286
- ```javascript
287
- import { createRevenue } from '@classytic/revenue';
288
- // Future: import { stripe } from '@classytic/revenue-stripe';
222
+ Map logical entities to transaction categories:
289
223
 
224
+ ```javascript
290
225
  const revenue = createRevenue({
291
226
  models: { Transaction },
292
- providers: {
293
- // Built-in manual provider is auto-included
294
- // stripe: stripe({ apiKey: process.env.STRIPE_KEY }),
227
+ config: {
228
+ categoryMappings: {
229
+ Order: 'order_subscription',
230
+ PlatformSubscription: 'platform_subscription',
231
+ Membership: 'gym_membership',
232
+ Enrollment: 'course_enrollment',
233
+ },
295
234
  },
296
235
  });
297
236
 
298
- // Create subscription with payment gateway
237
+ // Usage
299
238
  await revenue.subscriptions.create({
300
- data: { organizationId, customerId },
301
- planKey: 'monthly',
302
- amount: 99.99,
303
- gateway: 'stripe', // or 'manual'
239
+ entity: 'Order', // Maps to 'order_subscription' category
240
+ monetizationType: 'subscription',
241
+ // ...
304
242
  });
305
243
  ```
306
244
 
307
- ### With Hooks
245
+ **Note:** `entity` is a logical identifier (not a database model name) for organizing your business logic.
246
+
247
+ ## Hooks
308
248
 
309
249
  ```javascript
310
250
  const revenue = createRevenue({
311
251
  models: { Transaction },
312
252
  hooks: {
313
- 'payment.verified': async ({ transaction }) => {
314
- console.log('Payment verified:', transaction._id);
315
- // Send email, update analytics, etc.
316
- },
317
253
  'subscription.created': async ({ subscription, transaction }) => {
318
254
  console.log('New subscription:', subscription._id);
319
255
  },
256
+ 'payment.verified': async ({ transaction }) => {
257
+ // Send confirmation email
258
+ },
259
+ 'payment.refunded': async ({ refundTransaction }) => {
260
+ // Process refund notification
261
+ },
320
262
  },
321
263
  });
322
264
  ```
323
265
 
324
- ### Custom Logger
266
+ **Available hooks:**
267
+ - `subscription.created`, `subscription.activated`, `subscription.renewed`
268
+ - `subscription.paused`, `subscription.resumed`, `subscription.cancelled`
269
+ - `payment.verified`, `payment.refunded`
270
+ - `payment.webhook.{type}` (for webhook events)
325
271
 
326
- ```javascript
327
- import winston from 'winston';
328
-
329
- const revenue = createRevenue({
330
- models: { Transaction },
331
- logger: winston.createLogger({ /* config */ }),
332
- });
333
- ```
334
-
335
- ## Core API
336
-
337
- ### Services
338
-
339
- The `revenue` instance provides three focused services:
340
-
341
- #### Subscriptions
342
-
343
- ```javascript
344
- // Create subscription
345
- const { subscription, transaction, paymentIntent } = await revenue.subscriptions.create({
346
- data: { organizationId, customerId, ... },
347
- planKey: 'monthly',
348
- amount: 99.99,
349
- currency: 'USD',
350
- gateway: 'manual', // optional
351
- metadata: { /* ... */ }, // optional
352
- });
353
-
354
- // Renew subscription
355
- await revenue.subscriptions.renew(subscriptionId, { amount: 99.99 });
356
-
357
- // Activate subscription
358
- await revenue.subscriptions.activate(subscriptionId);
359
-
360
- // Cancel subscription
361
- await revenue.subscriptions.cancel(subscriptionId, { immediate: true });
362
-
363
- // Pause/Resume
364
- await revenue.subscriptions.pause(subscriptionId);
365
- await revenue.subscriptions.resume(subscriptionId);
366
-
367
- // Get/List
368
- await revenue.subscriptions.get(subscriptionId);
369
- await revenue.subscriptions.list(filters, options);
370
- ```
371
-
372
- #### Payments
373
-
374
- ```javascript
375
- // Verify payment
376
- const { transaction, paymentResult, status } = await revenue.payments.verify(
377
- paymentIntentId,
378
- { verifiedBy: userId }
379
- );
272
+ ## Building Payment Providers
380
273
 
381
- // Get payment status
382
- const { transaction, status, provider } = await revenue.payments.getStatus(paymentIntentId);
383
-
384
- // Refund payment
385
- const { transaction, refundResult } = await revenue.payments.refund(
386
- paymentId,
387
- amount, // optional, defaults to full refund
388
- { reason: 'Customer request' }
389
- );
390
-
391
- // Handle webhook
392
- const { event, transaction, status } = await revenue.payments.handleWebhook(
393
- 'stripe',
394
- payload,
395
- headers
396
- );
397
- ```
398
-
399
- #### Transactions
400
-
401
- ```javascript
402
- // Get transaction
403
- const transaction = await revenue.transactions.get(transactionId);
404
-
405
- // List transactions
406
- const { transactions, total, page, limit, pages } = await revenue.transactions.list(
407
- { organizationId, status: 'verified' },
408
- { limit: 50, skip: 0, sort: { createdAt: -1 } }
409
- );
410
-
411
- // Update transaction
412
- await revenue.transactions.update(transactionId, { notes: 'Updated' });
413
- ```
414
-
415
- **Note**: For analytics, exports, or complex queries, use Mongoose aggregations directly on your Transaction model. This keeps the service thin and focused.
416
-
417
- ### Providers
418
-
419
- ```javascript
420
- // Get specific provider
421
- const stripeProvider = revenue.getProvider('stripe');
422
-
423
- // Check capabilities
424
- const capabilities = stripeProvider.getCapabilities();
425
- // {
426
- // supportsWebhooks: true,
427
- // supportsRefunds: true,
428
- // supportsPartialRefunds: true,
429
- // requiresManualVerification: false
430
- // }
431
- ```
432
-
433
- ## Error Handling
434
-
435
- All errors are typed with codes for easy handling:
274
+ Create custom providers for Stripe, PayPal, etc.:
436
275
 
437
276
  ```javascript
438
- import {
439
- TransactionNotFoundError,
440
- ProviderNotFoundError,
441
- RefundNotSupportedError
442
- } from '@classytic/revenue';
277
+ import { PaymentProvider, PaymentIntent, PaymentResult } from '@classytic/revenue';
443
278
 
444
- try {
445
- await revenue.payments.verify(intentId);
446
- } catch (error) {
447
- if (error instanceof TransactionNotFoundError) {
448
- console.log('Transaction not found:', error.metadata.transactionId);
279
+ export class StripeProvider extends PaymentProvider {
280
+ constructor(config) {
281
+ super(config);
282
+ this.name = 'stripe';
283
+ this.stripe = new Stripe(config.apiKey);
449
284
  }
450
285
 
451
- if (error.code === 'TRANSACTION_NOT_FOUND') {
452
- // Handle specific error
286
+ async createIntent(params) {
287
+ const intent = await this.stripe.paymentIntents.create({
288
+ amount: params.amount,
289
+ currency: params.currency,
290
+ });
291
+
292
+ return new PaymentIntent({
293
+ id: intent.id,
294
+ provider: 'stripe',
295
+ status: intent.status,
296
+ amount: intent.amount,
297
+ currency: intent.currency,
298
+ clientSecret: intent.client_secret,
299
+ raw: intent,
300
+ });
453
301
  }
454
302
 
455
- if (error.retryable) {
456
- // Retry the operation
303
+ async verifyPayment(intentId) {
304
+ const intent = await this.stripe.paymentIntents.retrieve(intentId);
305
+ return new PaymentResult({
306
+ id: intent.id,
307
+ provider: 'stripe',
308
+ status: intent.status === 'succeeded' ? 'succeeded' : 'failed',
309
+ paidAt: new Date(),
310
+ raw: intent,
311
+ });
457
312
  }
313
+
314
+ // Implement: getStatus(), refund(), handleWebhook()
458
315
  }
459
316
  ```
460
317
 
461
- ### Error Classes
462
-
463
- - `RevenueError` - Base error class
464
- - `ConfigurationError` - Configuration issues
465
- - `ModelNotRegisteredError` - Model not provided
466
- - `ProviderError` - Provider-related errors
467
- - `ProviderNotFoundError` - Provider doesn't exist
468
- - `PaymentIntentCreationError` - Failed to create payment intent
469
- - `PaymentVerificationError` - Verification failed
470
- - `NotFoundError` - Resource not found
471
- - `TransactionNotFoundError` - Transaction not found
472
- - `SubscriptionNotFoundError` - Subscription not found
473
- - `ValidationError` - Validation failed
474
- - `InvalidAmountError` - Invalid amount
475
- - `MissingRequiredFieldError` - Required field missing
476
- - `StateError` - Invalid state
477
- - `AlreadyVerifiedError` - Already verified
478
- - `InvalidStateTransitionError` - Invalid state change
479
- - `RefundNotSupportedError` - Provider doesn't support refunds
480
- - `RefundError` - Refund failed
481
-
482
- ## Enums & Schemas
483
-
484
- ```javascript
485
- import {
486
- TRANSACTION_STATUS,
487
- PAYMENT_GATEWAY_TYPE,
488
- SUBSCRIPTION_STATUS,
489
- PLAN_KEYS,
490
- currentPaymentSchema,
491
- subscriptionInfoSchema,
492
- } from '@classytic/revenue';
493
-
494
- // Use in your models
495
- const organizationSchema = new Schema({
496
- currentPayment: currentPaymentSchema,
497
- subscription: subscriptionInfoSchema,
498
- });
499
- ```
318
+ **See:** [`docs/guides/PROVIDER_GUIDE.md`](../docs/guides/PROVIDER_GUIDE.md) for complete guide.
500
319
 
501
320
  ## TypeScript
502
321
 
503
322
  Full TypeScript support included:
504
323
 
505
324
  ```typescript
506
- import { createRevenue, Revenue, RevenueOptions } from '@classytic/revenue';
507
-
508
- const options: RevenueOptions = {
509
- models: { Transaction: TransactionModel },
510
- };
511
-
512
- const revenue: Revenue = createRevenue(options);
513
- ```
514
-
515
- ## Advanced Usage
516
-
517
- ### Custom Providers
518
-
519
- ```javascript
520
- import { PaymentProvider } from '@classytic/revenue';
521
-
522
- class MyCustomProvider extends PaymentProvider {
523
- name = 'my-gateway';
524
-
525
- async createIntent(params) {
526
- // Implementation
527
- }
325
+ import { createRevenue, Revenue, PaymentService } from '@classytic/revenue';
326
+ import { TRANSACTION_TYPE, TRANSACTION_STATUS } from '@classytic/revenue/enums';
528
327
 
529
- async verifyPayment(intentId) {
530
- // Implementation
531
- }
532
-
533
- getCapabilities() {
534
- return {
535
- supportsWebhooks: true,
536
- supportsRefunds: true,
537
- supportsPartialRefunds: false,
538
- requiresManualVerification: false,
539
- };
540
- }
541
- }
542
-
543
- const revenue = createRevenue({
328
+ const revenue: Revenue = createRevenue({
544
329
  models: { Transaction },
545
- providers: {
546
- 'my-gateway': new MyCustomProvider(),
547
- },
548
330
  });
549
- ```
550
-
551
- ### DI Container Access
552
-
553
- ```javascript
554
- const revenue = createRevenue({ models: { Transaction } });
555
331
 
556
- // Access container
557
- const models = revenue.container.get('models');
558
- const providers = revenue.container.get('providers');
332
+ // All services are fully typed
333
+ const payment = await revenue.payments.verify(id);
334
+ const subscription = await revenue.subscriptions.create({ ... });
559
335
  ```
560
336
 
561
- ## Hook Events
562
-
563
- Available hook events:
564
-
565
- - `payment.verified` - Payment verified
566
- - `payment.failed` - Payment failed
567
- - `subscription.created` - Subscription created
568
- - `subscription.renewed` - Subscription renewed
569
- - `subscription.activated` - Subscription activated
570
- - `subscription.cancelled` - Subscription cancelled
571
- - `subscription.paused` - Subscription paused
572
- - `subscription.resumed` - Subscription resumed
573
- - `transaction.created` - Transaction created
574
- - `transaction.updated` - Transaction updated
575
-
576
- Hooks are fire-and-forget - they never break the main flow. Errors are logged but don't throw.
577
-
578
- ## Architecture
579
-
580
- ```
581
- @classytic/revenue (core package)
582
- ├── Builder (createRevenue)
583
- ├── DI Container
584
- ├── Services (focused on lifecycle)
585
- │ ├── SubscriptionService
586
- │ ├── PaymentService
587
- │ └── TransactionService
588
- ├── Providers
589
- │ ├── base.js (interface)
590
- │ └── manual.js (built-in)
591
- ├── Error classes
592
- └── Schemas & Enums
593
-
594
- @classytic/revenue-stripe (future)
595
- @classytic/revenue-sslcommerz (future)
596
- @classytic/revenue-fastify (framework adapter, future)
597
- ```
337
+ ## Examples
598
338
 
599
- ## Design Principles
339
+ - [`examples/basic-usage.js`](examples/basic-usage.js) - Quick start guide
340
+ - [`examples/transaction.model.js`](examples/transaction.model.js) - Complete model setup
341
+ - [`examples/transaction-type-mapping.js`](examples/transaction-type-mapping.js) - Income/expense configuration
342
+ - [`examples/complete-flow.js`](examples/complete-flow.js) - Full lifecycle with state management
343
+ - [`examples/multivendor-platform.js`](examples/multivendor-platform.js) - Multi-tenant setup
600
344
 
601
- - **KISS**: Keep It Simple, Stupid
602
- - **DRY**: Don't Repeat Yourself
603
- - **SOLID**: Single responsibility, focused services
604
- - **Immutable**: Revenue instance is deeply frozen
605
- - **Thin Core**: Core operations only, users extend as needed
606
- - **Smart Defaults**: Works out-of-box with minimal config
607
-
608
- ## Migration from Legacy API
609
-
610
- If you're using the old `initializeRevenue()` API:
345
+ ## Error Handling
611
346
 
612
347
  ```javascript
613
- // Old (legacy API - removed)
614
- import { initializeRevenue, monetization, payment } from '@classytic/revenue';
615
- initializeRevenue({ TransactionModel, transactionService });
616
- await monetization.createSubscription(params);
348
+ import {
349
+ TransactionNotFoundError,
350
+ ProviderNotFoundError,
351
+ AlreadyVerifiedError,
352
+ RefundError,
353
+ } from '@classytic/revenue';
617
354
 
618
- // ✅ New (DI-based API)
619
- import { createRevenue } from '@classytic/revenue';
620
- const revenue = createRevenue({ models: { Transaction } });
621
- await revenue.subscriptions.create(params);
355
+ try {
356
+ await revenue.payments.verify(id);
357
+ } catch (error) {
358
+ if (error instanceof AlreadyVerifiedError) {
359
+ console.log('Already verified');
360
+ } else if (error instanceof TransactionNotFoundError) {
361
+ console.log('Transaction not found');
362
+ }
363
+ }
622
364
  ```
623
365
 
624
366
  ## Documentation
625
367
 
626
- - **[Building Payment Providers](../docs/guides/PROVIDER_GUIDE.md)** - Create custom payment integrations
627
- - **[Examples](../docs/examples/)** - Complete usage examples
628
- - **[Full Documentation](../docs/README.md)** - Comprehensive guides
368
+ - **[Provider Guide](../docs/guides/PROVIDER_GUIDE.md)** - Build custom payment providers
369
+ - **[Architecture](../docs/README.md#architecture)** - System design and patterns
370
+ - **[API Reference](../docs/README.md)** - Complete API documentation
629
371
 
630
372
  ## Support
631
373
 
632
- - **GitHub**: https://github.com/classytic/revenue
633
- - **Issues**: https://github.com/classytic/revenue/issues
634
- - **npm**: https://npmjs.com/package/@classytic/revenue
374
+ - **GitHub**: [classytic/revenue](https://github.com/classytic/revenue)
375
+ - **Issues**: [Report bugs](https://github.com/classytic/revenue/issues)
376
+ - **NPM**: [@classytic/revenue](https://www.npmjs.com/package/@classytic/revenue)
635
377
 
636
378
  ## License
637
379
 
638
- MIT © Classytic (Classytic)
639
-
640
- ---
641
-
642
- **Built with ❤️ following SOLID principles and industry best practices**
380
+ MIT © [Classytic](https://github.com/classytic)
package/enums/index.d.ts CHANGED
@@ -5,6 +5,13 @@
5
5
 
6
6
  // ============ TRANSACTION ENUMS ============
7
7
 
8
+ export const TRANSACTION_TYPE: {
9
+ readonly INCOME: 'income';
10
+ readonly EXPENSE: 'expense';
11
+ };
12
+
13
+ export const TRANSACTION_TYPE_VALUES: string[];
14
+
8
15
  export const TRANSACTION_STATUS: {
9
16
  readonly PENDING: 'pending';
10
17
  readonly PAYMENT_INITIATED: 'payment_initiated';
@@ -86,6 +93,8 @@ export const MONETIZATION_TYPE_VALUES: string[];
86
93
  // ============ DEFAULT EXPORT ============
87
94
 
88
95
  declare const _default: {
96
+ TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
97
+ TRANSACTION_TYPE_VALUES: typeof TRANSACTION_TYPE_VALUES;
89
98
  TRANSACTION_STATUS: typeof TRANSACTION_STATUS;
90
99
  TRANSACTION_STATUS_VALUES: typeof TRANSACTION_STATUS_VALUES;
91
100
  LIBRARY_CATEGORIES: typeof LIBRARY_CATEGORIES;
package/enums/index.js CHANGED
@@ -16,6 +16,8 @@ export * from './monetization.enums.js';
16
16
 
17
17
  // Default export for convenience
18
18
  import {
19
+ TRANSACTION_TYPE,
20
+ TRANSACTION_TYPE_VALUES,
19
21
  TRANSACTION_STATUS,
20
22
  TRANSACTION_STATUS_VALUES,
21
23
  LIBRARY_CATEGORIES,
@@ -45,6 +47,8 @@ import {
45
47
 
46
48
  export default {
47
49
  // Transaction enums
50
+ TRANSACTION_TYPE,
51
+ TRANSACTION_TYPE_VALUES,
48
52
  TRANSACTION_STATUS,
49
53
  TRANSACTION_STATUS_VALUES,
50
54
  LIBRARY_CATEGORIES,
@@ -6,6 +6,22 @@
6
6
  * Users should define their own categories and merge with these.
7
7
  */
8
8
 
9
+ // ============ TRANSACTION TYPE ============
10
+ /**
11
+ * Transaction Type - Income vs Expense
12
+ *
13
+ * INCOME: Money coming in (payments, subscriptions, purchases)
14
+ * EXPENSE: Money going out (refunds, payouts)
15
+ *
16
+ * Users can map these in their config via transactionTypeMapping
17
+ */
18
+ export const TRANSACTION_TYPE = {
19
+ INCOME: 'income',
20
+ EXPENSE: 'expense',
21
+ };
22
+
23
+ export const TRANSACTION_TYPE_VALUES = Object.values(TRANSACTION_TYPE);
24
+
9
25
  // ============ TRANSACTION STATUS ============
10
26
  /**
11
27
  * Transaction Status - Library-managed states
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/revenue",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
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
@@ -132,7 +132,7 @@ export class PaymentService {
132
132
 
133
133
  verify(paymentIntentId: string, options?: { verifiedBy?: string }): Promise<{ transaction: any; paymentResult: PaymentResult; status: string }>;
134
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; refundResult: RefundResult; status: string }>;
135
+ refund(paymentId: string, amount?: number, options?: { reason?: string }): Promise<{ transaction: any; refundTransaction: any; refundResult: RefundResult; status: string }>;
136
136
  handleWebhook(providerName: string, payload: any, headers?: any): Promise<{ event: WebhookEvent; transaction: any; status: string }>;
137
137
  list(filters?: any, options?: any): Promise<any[]>;
138
138
  get(transactionId: string): Promise<any>;
@@ -219,6 +219,25 @@ export interface RevenueOptions {
219
219
  * If not specified, falls back to library defaults: 'subscription' or 'purchase'
220
220
  */
221
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'>;
222
241
  [key: string]: any;
223
242
  };
224
243
  logger?: Console | any;
@@ -228,6 +247,11 @@ export function createRevenue(options: RevenueOptions): Revenue;
228
247
 
229
248
  // ============ ENUMS ============
230
249
 
250
+ export const TRANSACTION_TYPE: {
251
+ INCOME: 'income';
252
+ EXPENSE: 'expense';
253
+ };
254
+
231
255
  export const TRANSACTION_STATUS: {
232
256
  PENDING: 'pending';
233
257
  PAYMENT_INITIATED: 'payment_initiated';
@@ -16,6 +16,7 @@ import {
16
16
  ProviderCapabilityError,
17
17
  } from '../core/errors.js';
18
18
  import { triggerHook } from '../utils/hooks.js';
19
+ import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
19
20
 
20
21
  /**
21
22
  * Payment Service
@@ -221,15 +222,43 @@ export class PaymentService {
221
222
  throw new RefundError(paymentId, error.message);
222
223
  }
223
224
 
224
- // Update transaction
225
+ // Create separate refund transaction (EXPENSE) for proper accounting
226
+ const refundTransactionType = this.config.transactionTypeMapping?.refund || TRANSACTION_TYPE.EXPENSE;
227
+
228
+ const refundTransaction = await TransactionModel.create({
229
+ organizationId: transaction.organizationId,
230
+ customerId: transaction.customerId,
231
+ amount: refundAmount,
232
+ currency: transaction.currency,
233
+ category: transaction.category,
234
+ type: refundTransactionType, // EXPENSE - money going out
235
+ method: transaction.method || 'manual',
236
+ status: 'completed',
237
+ gateway: {
238
+ type: transaction.gateway?.type || 'manual',
239
+ paymentIntentId: refundResult.id,
240
+ provider: refundResult.provider,
241
+ },
242
+ paymentDetails: transaction.paymentDetails,
243
+ metadata: {
244
+ ...transaction.metadata,
245
+ isRefund: true,
246
+ originalTransactionId: transaction._id.toString(),
247
+ refundReason: reason,
248
+ refundResult: refundResult.metadata,
249
+ },
250
+ idempotencyKey: `refund_${transaction._id}_${Date.now()}`,
251
+ });
252
+
253
+ // Update original transaction status
225
254
  const isPartialRefund = refundAmount < transaction.amount;
226
255
  transaction.status = isPartialRefund ? 'partially_refunded' : 'refunded';
227
256
  transaction.refundedAmount = (transaction.refundedAmount || 0) + refundAmount;
228
257
  transaction.refundedAt = refundResult.refundedAt || new Date();
229
258
  transaction.metadata = {
230
259
  ...transaction.metadata,
260
+ refundTransactionId: refundTransaction._id.toString(),
231
261
  refundReason: reason,
232
- refundResult: refundResult.metadata,
233
262
  };
234
263
 
235
264
  await transaction.save();
@@ -237,6 +266,7 @@ export class PaymentService {
237
266
  // Trigger hook
238
267
  this._triggerHook('payment.refunded', {
239
268
  transaction,
269
+ refundTransaction,
240
270
  refundResult,
241
271
  refundAmount,
242
272
  reason,
@@ -245,6 +275,7 @@ export class PaymentService {
245
275
 
246
276
  return {
247
277
  transaction,
278
+ refundTransaction,
248
279
  refundResult,
249
280
  status: transaction.status,
250
281
  };
@@ -15,10 +15,12 @@ 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';
21
22
  import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
23
+ import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
22
24
 
23
25
  /**
24
26
  * Subscription Service
@@ -109,6 +111,11 @@ export class SubscriptionService {
109
111
  // Resolve category based on entity and monetizationType
110
112
  const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
111
113
 
114
+ // Resolve transaction type using config mapping or default to 'income'
115
+ const transactionType = this.config.transactionTypeMapping?.subscription
116
+ || this.config.transactionTypeMapping?.[monetizationType]
117
+ || TRANSACTION_TYPE.INCOME;
118
+
112
119
  // Create transaction record
113
120
  const TransactionModel = this.models.Transaction;
114
121
  transaction = await TransactionModel.create({
@@ -117,7 +124,8 @@ export class SubscriptionService {
117
124
  amount,
118
125
  currency,
119
126
  category,
120
- type: 'credit',
127
+ type: transactionType,
128
+ method: paymentData?.method || 'manual',
121
129
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
122
130
  gateway: {
123
131
  type: gateway,
@@ -285,6 +293,12 @@ export class SubscriptionService {
285
293
  const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
286
294
  const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
287
295
 
296
+ // Resolve transaction type using config mapping or default to 'income'
297
+ const transactionType = this.config.transactionTypeMapping?.subscription_renewal
298
+ || this.config.transactionTypeMapping?.subscription
299
+ || this.config.transactionTypeMapping?.[effectiveMonetizationType]
300
+ || TRANSACTION_TYPE.INCOME;
301
+
288
302
  // Create transaction
289
303
  const TransactionModel = this.models.Transaction;
290
304
  const transaction = await TransactionModel.create({
@@ -293,7 +307,8 @@ export class SubscriptionService {
293
307
  amount: subscription.amount,
294
308
  currency: subscription.currency || 'BDT',
295
309
  category,
296
- type: 'credit',
310
+ type: transactionType,
311
+ method: paymentData?.method || 'manual',
297
312
  status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
298
313
  gateway: {
299
314
  type: gateway,