@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 +229 -491
- package/enums/index.d.ts +9 -0
- package/enums/index.js +4 -0
- package/enums/transaction.enums.js +16 -0
- package/package.json +1 -1
- package/revenue.d.ts +25 -1
- package/services/payment.service.js +33 -2
- package/services/subscription.service.js +17 -2
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,
|
|
10
|
-
- **Payment Processing**: Multi-gateway support (Stripe, SSLCommerz,
|
|
11
|
-
- **Transaction Management**:
|
|
12
|
-
- **Provider Pattern**: Pluggable payment providers (like AI SDK)
|
|
13
|
-
- **Framework Agnostic**: Works with
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
//
|
|
54
|
-
await revenue.subscriptions.create({
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
**
|
|
52
|
+
**That's it!** Working revenue system in 3 steps.
|
|
68
53
|
|
|
69
54
|
## Transaction Model Setup
|
|
70
55
|
|
|
71
|
-
|
|
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
|
-
//
|
|
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,
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
gateway: gatewaySchema,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
95
|
+
## Available Schemas
|
|
119
96
|
|
|
120
|
-
|
|
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
|
-
|
|
105
|
+
**Usage:** Import and use as nested objects (NOT spread):
|
|
123
106
|
|
|
124
107
|
```javascript
|
|
125
|
-
import {
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
## Real-World Use Cases
|
|
116
|
+
## Core API
|
|
144
117
|
|
|
145
|
-
###
|
|
118
|
+
### Subscriptions
|
|
146
119
|
|
|
147
120
|
```javascript
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
###
|
|
150
|
+
### Payments
|
|
182
151
|
|
|
183
152
|
```javascript
|
|
184
|
-
//
|
|
185
|
-
const
|
|
186
|
-
|
|
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
|
-
//
|
|
197
|
-
await revenue.
|
|
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
|
-
//
|
|
206
|
-
await revenue.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
//
|
|
214
|
-
await revenue.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
###
|
|
176
|
+
### Transactions
|
|
223
177
|
|
|
224
178
|
```javascript
|
|
225
|
-
//
|
|
226
|
-
const
|
|
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
|
-
//
|
|
237
|
-
await revenue.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
//
|
|
246
|
-
await revenue.
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
+
## Custom Categories
|
|
285
221
|
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
//
|
|
237
|
+
// Usage
|
|
299
238
|
await revenue.subscriptions.create({
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
gateway: 'stripe', // or 'manual'
|
|
239
|
+
entity: 'Order', // Maps to 'order_subscription' category
|
|
240
|
+
monetizationType: 'subscription',
|
|
241
|
+
// ...
|
|
304
242
|
});
|
|
305
243
|
```
|
|
306
244
|
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
557
|
-
const
|
|
558
|
-
const
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
348
|
+
import {
|
|
349
|
+
TransactionNotFoundError,
|
|
350
|
+
ProviderNotFoundError,
|
|
351
|
+
AlreadyVerifiedError,
|
|
352
|
+
RefundError,
|
|
353
|
+
} from '@classytic/revenue';
|
|
617
354
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
- **[
|
|
627
|
-
- **[
|
|
628
|
-
- **[
|
|
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
|
-
- **
|
|
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
|
|
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
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
|
-
//
|
|
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:
|
|
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:
|
|
310
|
+
type: transactionType,
|
|
311
|
+
method: paymentData?.method || 'manual',
|
|
297
312
|
status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
|
|
298
313
|
gateway: {
|
|
299
314
|
type: gateway,
|