@classytic/revenue 1.0.6 → 1.1.2
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 +581 -633
- package/dist/application/services/index.d.ts +6 -0
- package/dist/application/services/index.js +3288 -0
- package/dist/application/services/index.js.map +1 -0
- package/dist/core/events.d.ts +455 -0
- package/dist/core/events.js +122 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/index.d.ts +12 -889
- package/dist/core/index.js +2372 -786
- package/dist/core/index.js.map +1 -1
- package/dist/enums/index.d.ts +29 -8
- package/dist/enums/index.js +41 -8
- package/dist/enums/index.js.map +1 -1
- package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
- package/dist/{index-BnEXsnLJ.d.ts → index-DxIK0UmZ.d.ts} +281 -26
- package/dist/index-EnfKzDbs.d.ts +806 -0
- package/dist/{index-C5SsOrV0.d.ts → index-cLJBLUvx.d.ts} +55 -111
- package/dist/index.d.ts +16 -16
- package/dist/index.js +2558 -2192
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/plugins/index.d.ts +267 -0
- package/dist/infrastructure/plugins/index.js +292 -0
- package/dist/infrastructure/plugins/index.js.map +1 -0
- package/dist/money-widWVD7r.d.ts +111 -0
- package/dist/payment.enums-C1BiGlRa.d.ts +69 -0
- package/dist/plugin-Bb9HOE10.d.ts +336 -0
- package/dist/providers/index.d.ts +19 -6
- package/dist/providers/index.js +22 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/reconciliation/index.d.ts +215 -0
- package/dist/reconciliation/index.js +140 -0
- package/dist/reconciliation/index.js.map +1 -0
- package/dist/{retry-80lBCmSe.d.ts → retry-D4hFUwVk.d.ts} +1 -41
- package/dist/schemas/index.d.ts +1653 -49
- package/dist/schemas/index.js +233 -19
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/validation.d.ts +4 -4
- package/dist/schemas/validation.js +16 -15
- package/dist/schemas/validation.js.map +1 -1
- package/dist/settlement.enums-ByC1x0ye.d.ts +130 -0
- package/dist/settlement.schema-CpamV7ZY.d.ts +343 -0
- package/dist/split.enums-DG3TxQf9.d.ts +42 -0
- package/dist/tax-CV8A0sxl.d.ts +60 -0
- package/dist/utils/index.d.ts +487 -13
- package/dist/utils/index.js +351 -289
- package/dist/utils/index.js.map +1 -1
- package/package.json +22 -9
- package/dist/actions-Ctf2XUL-.d.ts +0 -519
- package/dist/payment.enums-B_RwB8iR.d.ts +0 -184
- package/dist/services/index.d.ts +0 -3
- package/dist/services/index.js +0 -1702
- package/dist/services/index.js.map +0 -1
- package/dist/split.schema-DLVF3XBI.d.ts +0 -1122
- package/dist/transaction.enums-7uBnuswI.d.ts +0 -87
package/README.md
CHANGED
|
@@ -1,856 +1,804 @@
|
|
|
1
1
|
# @classytic/revenue
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **Universal financial ledger for SaaS & marketplaces**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Track subscriptions, purchases, refunds, escrow, and commission splits in **ONE Transaction model**. Built for enterprise with state machines, automatic retry logic, and multi-gateway support.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@classytic/revenue)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
npm install @classytic/revenue @classytic/revenue-manual
|
|
11
|
-
```
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## What Is This?
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
A TypeScript library that handles **all financial transactions** in one unified model:
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const revenue = Revenue
|
|
22
|
-
.create({ defaultCurrency: 'USD' })
|
|
23
|
-
.withModels({ Transaction, Subscription })
|
|
24
|
-
.withProvider('manual', new ManualProvider())
|
|
25
|
-
.withProvider('stripe', new StripeProvider({ apiKey: '...' }))
|
|
26
|
-
.withPlugin(loggingPlugin())
|
|
27
|
-
.withRetry({ maxAttempts: 3, baseDelay: 1000 })
|
|
28
|
-
.withCircuitBreaker()
|
|
29
|
-
.withCommission(10, 2.5) // 10% platform, 2.5% gateway fee
|
|
30
|
-
.forEnvironment('production')
|
|
31
|
-
.build();
|
|
18
|
+
// Subscription payment
|
|
19
|
+
{ type: 'subscription', flow: 'inflow', amount: 2999 }
|
|
32
20
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
await revenue.payments.verify(transactionId);
|
|
36
|
-
await revenue.escrow.hold(transactionId);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Shorthand Factory
|
|
21
|
+
// Product purchase
|
|
22
|
+
{ type: 'product_order', flow: 'inflow', amount: 1500 }
|
|
40
23
|
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
// Refund
|
|
25
|
+
{ type: 'refund', flow: 'outflow', amount: 1500 }
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
providers: { manual: new ManualProvider() },
|
|
47
|
-
options: { defaultCurrency: 'USD' },
|
|
48
|
-
});
|
|
27
|
+
// Operational expense
|
|
28
|
+
{ type: 'rent', flow: 'outflow', amount: 50000 }
|
|
49
29
|
```
|
|
50
30
|
|
|
31
|
+
**One table. Query by type. Calculate P&L. Track cash flow.**
|
|
32
|
+
|
|
51
33
|
---
|
|
34
|
+
## Unified Cashflow Model (Shared Types)
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
`@classytic/revenue` re-exports the unified transaction types from `@classytic/shared-types`. If you want a single Transaction model across revenue + payroll, define your schema using the shared types. The shared types are an interface only — you own the schema, enums, and indexes. There is no required “common schema”.
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
Type safety is provided by `ITransaction` only. Transaction categories (`type`) are app-defined; `flow` (`inflow`/`outflow`) is the only shared enum.
|
|
56
39
|
|
|
57
40
|
```typescript
|
|
58
|
-
import {
|
|
41
|
+
import type { ITransaction } from '@classytic/shared-types';
|
|
42
|
+
// or: import type { ITransaction } from '@classytic/revenue';
|
|
43
|
+
```
|
|
59
44
|
|
|
60
|
-
// Create from cents (safe)
|
|
61
|
-
const price = Money.usd(1999); // $19.99
|
|
62
|
-
const price2 = Money.of(19.99, 'USD'); // Auto-converts to 1999 cents
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
const discounted = price.multiply(0.9); // 10% off
|
|
66
|
-
const withTax = price.add(Money.usd(200));
|
|
67
|
-
const perPerson = price.divide(3);
|
|
46
|
+
## Why Use This?
|
|
68
47
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
48
|
+
**Instead of:**
|
|
49
|
+
- Separate tables for subscriptions, orders, refunds, invoices
|
|
50
|
+
- Scattered payment logic across your codebase
|
|
51
|
+
- Manual state management and validation
|
|
52
|
+
- Building payment provider integrations from scratch
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
54
|
+
**You get:**
|
|
55
|
+
- ✅ **ONE Transaction model** = Simpler schema, easier queries
|
|
56
|
+
- ✅ **State machines** = Prevents invalid transitions (can't refund a pending payment)
|
|
57
|
+
- ✅ **Provider abstraction** = Swap Stripe/PayPal/SSLCommerz without code changes
|
|
58
|
+
- ✅ **Production-ready** = Retry, circuit breaker, idempotency built-in
|
|
59
|
+
- ✅ **Plugins** = Optional tax, logging, audit trails
|
|
60
|
+
- ✅ **Type-safe** = Full TypeScript + Zod v4 validation
|
|
61
|
+
- ✅ **Integer money** = No floating-point errors
|
|
77
62
|
|
|
78
|
-
|
|
63
|
+
---
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
import { Result, ok, err, match } from '@classytic/revenue';
|
|
65
|
+
## When to Use This
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
| Use Case | Example |
|
|
68
|
+
|----------|---------|
|
|
69
|
+
| **SaaS billing** | Monthly/annual subscriptions with auto-renewal |
|
|
70
|
+
| **Marketplace payouts** | Creator platforms, affiliate commissions |
|
|
71
|
+
| **E-commerce** | Product purchases with refunds |
|
|
72
|
+
| **Escrow** | Hold funds until delivery/conditions met |
|
|
73
|
+
| **Multi-party splits** | Revenue sharing (70% creator, 20% affiliate, 10% platform) |
|
|
74
|
+
| **Financial reporting** | P&L statements, cash flow tracking |
|
|
88
75
|
|
|
89
|
-
|
|
90
|
-
match(result, {
|
|
91
|
-
ok: (value) => console.log('Success:', value),
|
|
92
|
-
err: (error) => console.log('Error:', error.message),
|
|
93
|
-
});
|
|
76
|
+
---
|
|
94
77
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
console.log(result.error);
|
|
100
|
-
}
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install @classytic/revenue mongoose zod
|
|
101
82
|
```
|
|
102
83
|
|
|
103
|
-
|
|
84
|
+
**Peer Dependencies:**
|
|
85
|
+
- `mongoose` ^8.0.0 || ^9.0.0
|
|
86
|
+
- `zod` ^4.1.13
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
revenue
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
88
|
+
**Provider Packages** (install as needed):
|
|
89
|
+
```bash
|
|
90
|
+
npm install @classytic/revenue-manual # For cash/bank transfers
|
|
91
|
+
# Coming soon: @classytic/revenue-stripe, @classytic/revenue-sslcommerz
|
|
92
|
+
```
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
sendEmail(event.subscription.customerId, 'Renewed!');
|
|
114
|
-
});
|
|
94
|
+
---
|
|
115
95
|
|
|
116
|
-
|
|
117
|
-
console.log('Released:', event.releasedAmount);
|
|
118
|
-
});
|
|
96
|
+
## Quick Start
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
revenue.on('*', (event) => {
|
|
122
|
-
analytics.track(event.type, event);
|
|
123
|
-
});
|
|
124
|
-
```
|
|
98
|
+
### 1. Define Your Transaction Model
|
|
125
99
|
|
|
126
|
-
|
|
100
|
+
Copy the complete model from [examples/05-transaction-model.ts](./examples/05-transaction-model.ts):
|
|
127
101
|
|
|
128
102
|
```typescript
|
|
103
|
+
import mongoose, { Schema } from 'mongoose';
|
|
104
|
+
import type { ITransaction } from '@classytic/shared-types';
|
|
129
105
|
import {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
safeValidate,
|
|
135
|
-
validateSplitPayments,
|
|
106
|
+
TRANSACTION_FLOW_VALUES,
|
|
107
|
+
TRANSACTION_STATUS_VALUES,
|
|
108
|
+
gatewaySchema,
|
|
109
|
+
commissionSchema,
|
|
136
110
|
} from '@classytic/revenue';
|
|
137
111
|
|
|
138
|
-
//
|
|
139
|
-
const
|
|
112
|
+
// Your business categories
|
|
113
|
+
const CATEGORIES = {
|
|
114
|
+
PLATFORM_SUBSCRIPTION: 'platform_subscription',
|
|
115
|
+
COURSE_ENROLLMENT: 'course_enrollment',
|
|
116
|
+
PRODUCT_ORDER: 'product_order',
|
|
117
|
+
REFUND: 'refund',
|
|
118
|
+
RENT: 'rent',
|
|
119
|
+
SALARY: 'salary',
|
|
120
|
+
};
|
|
140
121
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
122
|
+
const transactionSchema = new Schema<ITransaction>({
|
|
123
|
+
organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
|
|
124
|
+
customerId: { type: Schema.Types.ObjectId, ref: 'Customer' },
|
|
125
|
+
sourceId: { type: Schema.Types.ObjectId },
|
|
126
|
+
sourceModel: { type: String }, // your app’s model name
|
|
127
|
+
type: { type: String, enum: Object.values(CATEGORIES), required: true }, // category
|
|
128
|
+
flow: { type: String, enum: TRANSACTION_FLOW_VALUES, required: true },
|
|
129
|
+
status: { type: String, enum: TRANSACTION_STATUS_VALUES, default: 'pending' },
|
|
130
|
+
amount: { type: Number, required: true },
|
|
131
|
+
currency: { type: String, default: 'USD' },
|
|
132
|
+
method: { type: String, required: true },
|
|
133
|
+
gateway: gatewaySchema,
|
|
134
|
+
commission: commissionSchema,
|
|
135
|
+
// ... see full model in examples
|
|
136
|
+
}, { timestamps: true });
|
|
146
137
|
|
|
147
|
-
|
|
148
|
-
const splitResult = safeValidate(CurrentPaymentInputSchema, {
|
|
149
|
-
amount: 50000,
|
|
150
|
-
method: 'split',
|
|
151
|
-
payments: [
|
|
152
|
-
{ method: 'cash', amount: 25000 },
|
|
153
|
-
{ method: 'bkash', amount: 25000 },
|
|
154
|
-
],
|
|
155
|
-
});
|
|
138
|
+
export const Transaction = mongoose.model('Transaction', transactionSchema);
|
|
156
139
|
```
|
|
157
140
|
|
|
158
|
-
|
|
141
|
+
When you call `monetization.create`, you can optionally pass `sourceId`/`sourceModel` in the input; revenue stores those as `sourceId`/`sourceModel` on the transaction for unified cashflow queries. If you create transactions yourself, set `sourceId`/`sourceModel` directly.
|
|
142
|
+
|
|
143
|
+
### 2. Initialize Revenue
|
|
159
144
|
|
|
160
|
-
|
|
145
|
+
```typescript
|
|
146
|
+
import { Revenue } from '@classytic/revenue';
|
|
147
|
+
import { ManualProvider } from '@classytic/revenue-manual';
|
|
148
|
+
|
|
149
|
+
const revenue = Revenue.create({
|
|
150
|
+
defaultCurrency: 'USD',
|
|
151
|
+
commissionRate: 0.10, // 10% platform fee
|
|
152
|
+
gatewayFeeRate: 0.029, // 2.9% payment processor
|
|
153
|
+
})
|
|
154
|
+
.withModels({ Transaction })
|
|
155
|
+
.withProvider('manual', new ManualProvider())
|
|
156
|
+
.build();
|
|
157
|
+
```
|
|
161
158
|
|
|
162
|
-
###
|
|
159
|
+
### 3. Create a Payment
|
|
163
160
|
|
|
164
161
|
```typescript
|
|
165
|
-
//
|
|
166
|
-
const { transaction,
|
|
162
|
+
// Create subscription payment
|
|
163
|
+
const { transaction, subscription } = await revenue.monetization.create({
|
|
167
164
|
data: {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
referenceId: order._id,
|
|
171
|
-
referenceModel: 'Order',
|
|
165
|
+
organizationId: 'org_123',
|
|
166
|
+
customerId: 'user_456',
|
|
172
167
|
},
|
|
173
|
-
planKey: 'one_time',
|
|
174
|
-
monetizationType: 'purchase',
|
|
175
|
-
amount: 1500,
|
|
176
|
-
gateway: 'manual',
|
|
177
|
-
paymentData: { method: 'card' },
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Recurring subscription
|
|
181
|
-
const { subscription, transaction } = await revenue.monetization.create({
|
|
182
|
-
data: { customerId: user._id },
|
|
183
168
|
planKey: 'monthly',
|
|
184
169
|
monetizationType: 'subscription',
|
|
185
|
-
amount: 2999,
|
|
186
|
-
gateway: '
|
|
170
|
+
amount: 2999, // $29.99 in cents
|
|
171
|
+
gateway: 'manual',
|
|
187
172
|
});
|
|
188
173
|
|
|
189
|
-
//
|
|
190
|
-
await revenue.monetization.activate(subscription._id);
|
|
191
|
-
await revenue.monetization.renew(subscription._id, { gateway: 'stripe' });
|
|
192
|
-
await revenue.monetization.pause(subscription._id, { reason: 'Vacation' });
|
|
193
|
-
await revenue.monetization.resume(subscription._id);
|
|
194
|
-
await revenue.monetization.cancel(subscription._id, { immediate: true });
|
|
174
|
+
console.log(transaction.status); // 'pending'
|
|
195
175
|
```
|
|
196
176
|
|
|
197
|
-
###
|
|
177
|
+
### 4. Verify Payment
|
|
198
178
|
|
|
199
179
|
```typescript
|
|
200
|
-
|
|
201
|
-
const { transaction, paymentResult } = await revenue.payments.verify(
|
|
202
|
-
transactionId,
|
|
203
|
-
{ verifiedBy: adminId }
|
|
204
|
-
);
|
|
180
|
+
await revenue.payments.verify(transaction._id);
|
|
205
181
|
|
|
206
|
-
//
|
|
207
|
-
|
|
182
|
+
// Transaction: 'pending' → 'verified'
|
|
183
|
+
// Subscription: 'pending' → 'active'
|
|
184
|
+
```
|
|
208
185
|
|
|
209
|
-
|
|
210
|
-
const { refundTransaction } = await revenue.payments.refund(transactionId);
|
|
186
|
+
### 5. Handle Refunds
|
|
211
187
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
500, // Amount in cents
|
|
216
|
-
{ reason: 'Partial return' }
|
|
217
|
-
);
|
|
188
|
+
```typescript
|
|
189
|
+
// Full refund
|
|
190
|
+
await revenue.payments.refund(transaction._id);
|
|
218
191
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
'
|
|
222
|
-
|
|
223
|
-
headers
|
|
224
|
-
);
|
|
192
|
+
// Partial refund: $10.00
|
|
193
|
+
await revenue.payments.refund(transaction._id, 1000, {
|
|
194
|
+
reason: 'customer_request',
|
|
195
|
+
});
|
|
225
196
|
```
|
|
226
197
|
|
|
227
|
-
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Core Concepts
|
|
201
|
+
|
|
202
|
+
### 1. Transaction Model (Required)
|
|
203
|
+
|
|
204
|
+
**The universal ledger.** Every financial event becomes a transaction:
|
|
228
205
|
|
|
229
206
|
```typescript
|
|
230
|
-
//
|
|
231
|
-
await
|
|
232
|
-
|
|
233
|
-
|
|
207
|
+
// Query subscriptions
|
|
208
|
+
const subscriptions = await Transaction.find({
|
|
209
|
+
type: 'platform_subscription',
|
|
210
|
+
status: 'verified'
|
|
234
211
|
});
|
|
235
212
|
|
|
236
|
-
//
|
|
237
|
-
await
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
213
|
+
// Calculate revenue
|
|
214
|
+
const income = await Transaction.aggregate([
|
|
215
|
+
{ $match: { flow: 'inflow', status: 'verified' } },
|
|
216
|
+
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
217
|
+
]);
|
|
242
218
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{
|
|
246
|
-
{ type: 'affiliate_commission', recipientId: 'aff_123', rate: 0.05 },
|
|
219
|
+
const expenses = await Transaction.aggregate([
|
|
220
|
+
{ $match: { flow: 'outflow', status: 'verified' } },
|
|
221
|
+
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
247
222
|
]);
|
|
248
223
|
|
|
249
|
-
|
|
250
|
-
await revenue.escrow.cancelHold(transactionId, { reason: 'Order cancelled' });
|
|
224
|
+
const netRevenue = income[0].total - expenses[0].total;
|
|
251
225
|
```
|
|
252
226
|
|
|
253
|
-
|
|
227
|
+
### 2. Payment Providers (Required)
|
|
254
228
|
|
|
255
|
-
|
|
229
|
+
**How money flows in.** Providers are swappable:
|
|
256
230
|
|
|
257
231
|
```typescript
|
|
258
|
-
import {
|
|
232
|
+
import { ManualProvider } from '@classytic/revenue-manual';
|
|
233
|
+
// import { StripeProvider } from '@classytic/revenue-stripe'; // Coming soon
|
|
259
234
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.
|
|
263
|
-
.withPlugin(loggingPlugin({ level: 'info' }))
|
|
264
|
-
.withPlugin(auditPlugin({ store: saveToDatabase }))
|
|
265
|
-
.withPlugin(metricsPlugin({ onMetric: sendToDatadog }))
|
|
266
|
-
.build();
|
|
235
|
+
revenue
|
|
236
|
+
.withProvider('manual', new ManualProvider())
|
|
237
|
+
.withProvider('stripe', new StripeProvider({ apiKey: '...' }));
|
|
267
238
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
'payment.create.before': async (ctx, input, next) => {
|
|
273
|
-
if (await isRateLimited(input.customerId)) {
|
|
274
|
-
throw new Error('Rate limited');
|
|
275
|
-
}
|
|
276
|
-
return next();
|
|
277
|
-
},
|
|
278
|
-
},
|
|
239
|
+
// Use any provider
|
|
240
|
+
await revenue.monetization.create({
|
|
241
|
+
gateway: 'manual', // or 'stripe'
|
|
242
|
+
// ...
|
|
279
243
|
});
|
|
280
244
|
```
|
|
281
245
|
|
|
282
|
-
|
|
246
|
+
### 3. Plugins (Optional)
|
|
283
247
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
### Retry with Exponential Backoff
|
|
248
|
+
**Extend behavior.** Plugins add features without coupling:
|
|
287
249
|
|
|
288
250
|
```typescript
|
|
289
|
-
import {
|
|
290
|
-
|
|
291
|
-
// Simple retry
|
|
292
|
-
const data = await retry(
|
|
293
|
-
() => fetchPaymentStatus(id),
|
|
294
|
-
{
|
|
295
|
-
maxAttempts: 5,
|
|
296
|
-
baseDelay: 1000,
|
|
297
|
-
maxDelay: 30000,
|
|
298
|
-
backoffMultiplier: 2,
|
|
299
|
-
jitter: 0.1,
|
|
300
|
-
}
|
|
301
|
-
);
|
|
251
|
+
import { loggingPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
|
|
302
252
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
253
|
+
revenue
|
|
254
|
+
.withPlugin(loggingPlugin({ level: 'info' }))
|
|
255
|
+
.withPlugin(createTaxPlugin({
|
|
256
|
+
getTaxConfig: async (orgId) => ({
|
|
257
|
+
isRegistered: true,
|
|
258
|
+
defaultRate: 0.15, // 15% tax
|
|
259
|
+
pricesIncludeTax: false,
|
|
260
|
+
}),
|
|
261
|
+
}));
|
|
308
262
|
```
|
|
309
263
|
|
|
310
|
-
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Common Operations
|
|
311
267
|
|
|
312
|
-
|
|
313
|
-
import { CircuitBreaker, createCircuitBreaker } from '@classytic/revenue';
|
|
268
|
+
### Create Subscription
|
|
314
269
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
270
|
+
```typescript
|
|
271
|
+
const { subscription, transaction } = await revenue.monetization.create({
|
|
272
|
+
data: {
|
|
273
|
+
organizationId: 'org_123',
|
|
274
|
+
customerId: 'user_456',
|
|
275
|
+
},
|
|
276
|
+
planKey: 'monthly_premium',
|
|
277
|
+
monetizationType: 'subscription',
|
|
278
|
+
amount: 2999, // $29.99/month
|
|
279
|
+
gateway: 'manual',
|
|
318
280
|
});
|
|
319
281
|
|
|
320
|
-
|
|
282
|
+
// Later: Renew
|
|
283
|
+
await revenue.monetization.renew(subscription._id);
|
|
321
284
|
|
|
322
|
-
//
|
|
323
|
-
|
|
285
|
+
// Cancel
|
|
286
|
+
await revenue.monetization.cancel(subscription._id, {
|
|
287
|
+
reason: 'customer_requested',
|
|
288
|
+
});
|
|
324
289
|
```
|
|
325
290
|
|
|
326
|
-
###
|
|
291
|
+
### Create One-Time Purchase
|
|
327
292
|
|
|
328
293
|
```typescript
|
|
329
|
-
|
|
294
|
+
const { transaction } = await revenue.monetization.create({
|
|
295
|
+
data: {
|
|
296
|
+
organizationId: 'org_123',
|
|
297
|
+
customerId: 'user_456',
|
|
298
|
+
sourceId: order._id, // optional: stored as sourceId
|
|
299
|
+
sourceModel: 'Order', // optional: stored as sourceModel
|
|
300
|
+
},
|
|
301
|
+
planKey: 'one_time',
|
|
302
|
+
monetizationType: 'purchase',
|
|
303
|
+
amount: 10000, // $100.00
|
|
304
|
+
gateway: 'manual',
|
|
305
|
+
});
|
|
306
|
+
```
|
|
330
307
|
|
|
331
|
-
|
|
308
|
+
### Query Transactions
|
|
332
309
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
310
|
+
```typescript
|
|
311
|
+
// By type (category)
|
|
312
|
+
const subscriptions = await Transaction.find({
|
|
313
|
+
type: 'platform_subscription',
|
|
314
|
+
status: 'verified',
|
|
315
|
+
});
|
|
338
316
|
|
|
339
|
-
//
|
|
340
|
-
|
|
317
|
+
// By source (sourceId/sourceModel on the transaction)
|
|
318
|
+
const orderPayments = await Transaction.find({
|
|
319
|
+
sourceModel: 'Order',
|
|
320
|
+
sourceId: orderId,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// By customer
|
|
324
|
+
const customerTransactions = await Transaction.find({
|
|
325
|
+
customerId: userId,
|
|
326
|
+
flow: 'inflow',
|
|
327
|
+
}).sort({ createdAt: -1 });
|
|
341
328
|
```
|
|
342
329
|
|
|
343
330
|
---
|
|
344
331
|
|
|
345
|
-
##
|
|
332
|
+
## Advanced Features
|
|
346
333
|
|
|
347
|
-
|
|
334
|
+
### State Machines (Data Integrity)
|
|
348
335
|
|
|
349
|
-
|
|
336
|
+
Prevent invalid transitions automatically:
|
|
350
337
|
|
|
351
338
|
```typescript
|
|
352
|
-
import
|
|
353
|
-
import {
|
|
354
|
-
// Enums
|
|
355
|
-
TRANSACTION_TYPE_VALUES,
|
|
356
|
-
TRANSACTION_STATUS_VALUES,
|
|
357
|
-
// Mongoose schemas (compose into your model)
|
|
358
|
-
gatewaySchema,
|
|
359
|
-
paymentDetailsSchema,
|
|
360
|
-
commissionSchema,
|
|
361
|
-
holdSchema,
|
|
362
|
-
splitSchema,
|
|
363
|
-
} from '@classytic/revenue';
|
|
339
|
+
import { TRANSACTION_STATE_MACHINE } from '@classytic/revenue';
|
|
364
340
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
'platform_subscription',
|
|
368
|
-
'course_enrollment',
|
|
369
|
-
'product_order',
|
|
370
|
-
'refund',
|
|
371
|
-
'rent',
|
|
372
|
-
'salary',
|
|
373
|
-
'utilities',
|
|
374
|
-
];
|
|
375
|
-
|
|
376
|
-
const transactionSchema = new mongoose.Schema({
|
|
377
|
-
// Core fields
|
|
378
|
-
organizationId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
|
|
379
|
-
customerId: { type: mongoose.Schema.Types.ObjectId, index: true },
|
|
380
|
-
type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true }, // income | expense
|
|
381
|
-
category: { type: String, enum: CATEGORIES, index: true },
|
|
382
|
-
status: { type: String, enum: TRANSACTION_STATUS_VALUES, default: 'pending' },
|
|
383
|
-
amount: { type: Number, required: true, min: 0 },
|
|
384
|
-
currency: { type: String, default: 'USD' },
|
|
385
|
-
method: { type: String, required: true },
|
|
341
|
+
// ✅ Valid
|
|
342
|
+
await revenue.payments.verify(transaction._id); // pending → verified
|
|
386
343
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
commission: commissionSchema,
|
|
390
|
-
paymentDetails: paymentDetailsSchema,
|
|
391
|
-
hold: holdSchema,
|
|
392
|
-
splits: [splitSchema],
|
|
393
|
-
|
|
394
|
-
// Polymorphic reference (link to any entity)
|
|
395
|
-
referenceId: { type: mongoose.Schema.Types.ObjectId, refPath: 'referenceModel' },
|
|
396
|
-
referenceModel: { type: String, enum: ['Subscription', 'Order', 'Enrollment'] },
|
|
397
|
-
|
|
398
|
-
// Idempotency & verification
|
|
399
|
-
idempotencyKey: { type: String, unique: true, sparse: true },
|
|
400
|
-
verifiedAt: Date,
|
|
401
|
-
verifiedBy: mongoose.Schema.Types.Mixed, // ObjectId or 'system'
|
|
402
|
-
|
|
403
|
-
// Refunds
|
|
404
|
-
refundedAmount: Number,
|
|
405
|
-
refundedAt: Date,
|
|
406
|
-
|
|
407
|
-
metadata: mongoose.Schema.Types.Mixed,
|
|
408
|
-
}, { timestamps: true });
|
|
344
|
+
// ❌ Invalid (throws InvalidStateTransitionError)
|
|
345
|
+
await revenue.payments.verify(completedTransaction._id); // completed → verified
|
|
409
346
|
|
|
410
|
-
|
|
347
|
+
// Check if transition is valid
|
|
348
|
+
const canRefund = TRANSACTION_STATE_MACHINE.canTransition(
|
|
349
|
+
transaction.status,
|
|
350
|
+
'refunded'
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Get allowed next states
|
|
354
|
+
const allowed = TRANSACTION_STATE_MACHINE.getAllowedTransitions('verified');
|
|
355
|
+
// ['completed', 'refunded', 'partially_refunded', 'cancelled']
|
|
356
|
+
|
|
357
|
+
// Check if state is terminal
|
|
358
|
+
const isDone = TRANSACTION_STATE_MACHINE.isTerminalState('refunded'); // true
|
|
411
359
|
```
|
|
412
360
|
|
|
413
|
-
|
|
361
|
+
**Available State Machines:**
|
|
362
|
+
- `TRANSACTION_STATE_MACHINE` - Payment lifecycle
|
|
363
|
+
- `SUBSCRIPTION_STATE_MACHINE` - Subscription states
|
|
364
|
+
- `SETTLEMENT_STATE_MACHINE` - Payout tracking
|
|
365
|
+
- `HOLD_STATE_MACHINE` - Escrow holds
|
|
366
|
+
- `SPLIT_STATE_MACHINE` - Revenue splits
|
|
414
367
|
|
|
415
|
-
|
|
416
|
-
|--------|---------|-------|
|
|
417
|
-
| `gatewaySchema` | Payment gateway details | `gateway: gatewaySchema` |
|
|
418
|
-
| `commissionSchema` | Platform commission | `commission: commissionSchema` |
|
|
419
|
-
| `paymentDetailsSchema` | Manual payment info | `paymentDetails: paymentDetailsSchema` |
|
|
420
|
-
| `holdSchema` | Escrow hold/release | `hold: holdSchema` |
|
|
421
|
-
| `splitSchema` | Multi-party splits | `splits: [splitSchema]` |
|
|
422
|
-
| `currentPaymentSchema` | For Order/Subscription models | `currentPayment: currentPaymentSchema` |
|
|
423
|
-
| `paymentEntrySchema` | Individual payment in split payments | Used within `currentPaymentSchema.payments` |
|
|
368
|
+
### Audit Trail (Track State Changes)
|
|
424
369
|
|
|
425
|
-
|
|
370
|
+
Every state transition is automatically logged:
|
|
426
371
|
|
|
427
372
|
```typescript
|
|
428
|
-
import {
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
373
|
+
import { getAuditTrail } from '@classytic/revenue';
|
|
374
|
+
|
|
375
|
+
const transaction = await Transaction.findById(txId);
|
|
376
|
+
const history = getAuditTrail(transaction);
|
|
377
|
+
|
|
378
|
+
console.log(history);
|
|
379
|
+
// [
|
|
380
|
+
// {
|
|
381
|
+
// resourceType: 'transaction',
|
|
382
|
+
// fromState: 'pending',
|
|
383
|
+
// toState: 'verified',
|
|
384
|
+
// changedAt: 2025-01-15T10:30:00.000Z,
|
|
385
|
+
// changedBy: 'admin_123',
|
|
386
|
+
// reason: 'Payment verified'
|
|
387
|
+
// }
|
|
388
|
+
// ]
|
|
435
389
|
```
|
|
436
390
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
## Group Payments (Split Pay)
|
|
391
|
+
### Escrow (Marketplaces)
|
|
440
392
|
|
|
441
|
-
|
|
393
|
+
Hold funds until conditions met:
|
|
442
394
|
|
|
443
395
|
```typescript
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
//
|
|
449
|
-
await revenue.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
organizationId: restaurantId,
|
|
453
|
-
referenceId: orderId,
|
|
454
|
-
referenceModel: 'Order',
|
|
455
|
-
},
|
|
456
|
-
planKey: 'split_payment',
|
|
457
|
-
monetizationType: 'purchase',
|
|
458
|
-
amount: 4000,
|
|
459
|
-
gateway: 'stripe',
|
|
460
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
396
|
+
// Create & verify transaction
|
|
397
|
+
const { transaction } = await revenue.monetization.create({ amount: 10000, ... });
|
|
398
|
+
await revenue.payments.verify(transaction._id);
|
|
399
|
+
|
|
400
|
+
// Hold in escrow
|
|
401
|
+
await revenue.escrow.hold(transaction._id, {
|
|
402
|
+
reason: 'pending_delivery',
|
|
403
|
+
holdUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
|
461
404
|
});
|
|
462
405
|
|
|
463
|
-
//
|
|
464
|
-
await revenue.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
referenceId: orderId,
|
|
469
|
-
referenceModel: 'Order',
|
|
470
|
-
},
|
|
471
|
-
planKey: 'split_payment',
|
|
472
|
-
monetizationType: 'purchase',
|
|
473
|
-
amount: 3500,
|
|
474
|
-
gateway: 'stripe',
|
|
475
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// Friend 3 pays $25
|
|
479
|
-
await revenue.monetization.create({
|
|
480
|
-
data: {
|
|
481
|
-
customerId: friend3,
|
|
482
|
-
organizationId: restaurantId,
|
|
483
|
-
referenceId: orderId,
|
|
484
|
-
referenceModel: 'Order',
|
|
485
|
-
},
|
|
486
|
-
planKey: 'split_payment',
|
|
487
|
-
monetizationType: 'purchase',
|
|
488
|
-
amount: 2500,
|
|
489
|
-
gateway: 'stripe',
|
|
490
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
406
|
+
// Release to seller after delivery confirmed
|
|
407
|
+
await revenue.escrow.release(transaction._id, {
|
|
408
|
+
recipientId: 'seller_123',
|
|
409
|
+
recipientType: 'organization',
|
|
410
|
+
reason: 'delivery_confirmed',
|
|
491
411
|
});
|
|
492
412
|
```
|
|
493
413
|
|
|
494
|
-
###
|
|
414
|
+
### Commission Splits (Affiliates)
|
|
415
|
+
|
|
416
|
+
Split revenue between multiple parties:
|
|
495
417
|
|
|
496
418
|
```typescript
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
419
|
+
await revenue.escrow.split(transaction._id, {
|
|
420
|
+
splits: [
|
|
421
|
+
{ recipientId: 'creator_123', recipientType: 'user', percentage: 70 },
|
|
422
|
+
{ recipientId: 'affiliate_456', recipientType: 'user', percentage: 10 },
|
|
423
|
+
],
|
|
424
|
+
organizationPercentage: 20, // Platform keeps 20%
|
|
501
425
|
});
|
|
502
426
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const isFullyPaid = totalPaid >= orderTotal;
|
|
508
|
-
|
|
509
|
-
console.log({
|
|
510
|
-
totalPaid, // 10000
|
|
511
|
-
remaining, // 0
|
|
512
|
-
isFullyPaid, // true
|
|
513
|
-
payers: verified.map(t => ({
|
|
514
|
-
customerId: t.customerId,
|
|
515
|
-
amount: t.amount,
|
|
516
|
-
paidAt: t.verifiedAt,
|
|
517
|
-
})),
|
|
518
|
-
});
|
|
427
|
+
// Creates 3 transactions:
|
|
428
|
+
// - Creator: $70.00
|
|
429
|
+
// - Affiliate: $10.00
|
|
430
|
+
// - Platform: $20.00
|
|
519
431
|
```
|
|
520
432
|
|
|
521
|
-
###
|
|
433
|
+
### Events (React to Changes)
|
|
522
434
|
|
|
523
435
|
```typescript
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
436
|
+
import { EventBus } from '@classytic/revenue/events';
|
|
437
|
+
|
|
438
|
+
revenue.events.on('payment:verified', async (event) => {
|
|
439
|
+
// Grant access
|
|
440
|
+
await grantAccess(event.transaction.customerId);
|
|
528
441
|
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
referenceId: orderId,
|
|
532
|
-
status: 'pending',
|
|
442
|
+
// Send email
|
|
443
|
+
await sendEmail(event.transaction.customerId, 'Payment received!');
|
|
533
444
|
});
|
|
534
|
-
```
|
|
535
445
|
|
|
536
|
-
|
|
446
|
+
revenue.events.on('subscription:cancelled', async (event) => {
|
|
447
|
+
await removeAccess(event.subscription.customerId);
|
|
448
|
+
});
|
|
537
449
|
|
|
538
|
-
|
|
450
|
+
// Other events:
|
|
451
|
+
// - monetization:created, payment:failed, payment:refunded
|
|
452
|
+
// - subscription:activated, subscription:renewed
|
|
453
|
+
// - escrow:held, escrow:released, settlement:completed
|
|
454
|
+
```
|
|
539
455
|
|
|
540
|
-
|
|
456
|
+
### Tax Plugin (Optional)
|
|
541
457
|
|
|
542
|
-
|
|
458
|
+
Automatically calculate and track tax:
|
|
543
459
|
|
|
544
460
|
```typescript
|
|
545
|
-
import {
|
|
461
|
+
import { createTaxPlugin } from '@classytic/revenue/plugins';
|
|
546
462
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
463
|
+
const revenue = Revenue.create()
|
|
464
|
+
.withModels({ Transaction })
|
|
465
|
+
.withProvider('manual', new ManualProvider())
|
|
466
|
+
.withPlugin(createTaxPlugin({
|
|
467
|
+
getTaxConfig: async (organizationId) => ({
|
|
468
|
+
isRegistered: true,
|
|
469
|
+
defaultRate: 0.15, // 15% tax
|
|
470
|
+
pricesIncludeTax: false, // Tax-exclusive pricing
|
|
471
|
+
exemptCategories: ['education', 'donation'],
|
|
472
|
+
}),
|
|
473
|
+
}))
|
|
474
|
+
.build();
|
|
475
|
+
|
|
476
|
+
// Tax calculated automatically
|
|
477
|
+
const { transaction } = await revenue.monetization.create({
|
|
478
|
+
amount: 10000, // $100.00
|
|
550
479
|
// ...
|
|
551
480
|
});
|
|
481
|
+
|
|
482
|
+
console.log(transaction.tax);
|
|
483
|
+
// {
|
|
484
|
+
// rate: 0.15,
|
|
485
|
+
// baseAmount: 10000,
|
|
486
|
+
// taxAmount: 1500, // $15.00
|
|
487
|
+
// totalAmount: 11500, // $115.00
|
|
488
|
+
// }
|
|
489
|
+
|
|
490
|
+
// Tax automatically reversed on refunds
|
|
491
|
+
await revenue.payments.refund(transaction._id);
|
|
552
492
|
```
|
|
553
493
|
|
|
554
|
-
###
|
|
494
|
+
### Custom Plugins
|
|
555
495
|
|
|
556
496
|
```typescript
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
497
|
+
import { definePlugin } from '@classytic/revenue/plugins';
|
|
498
|
+
|
|
499
|
+
const notificationPlugin = definePlugin({
|
|
500
|
+
name: 'notifications',
|
|
501
|
+
version: '1.0.0',
|
|
502
|
+
hooks: {
|
|
503
|
+
'payment.verify.after': async (ctx, input, next) => {
|
|
504
|
+
const result = await next();
|
|
505
|
+
|
|
506
|
+
// Send notification
|
|
507
|
+
await sendPushNotification({
|
|
508
|
+
userId: result.transaction.customerId,
|
|
509
|
+
message: 'Payment verified!',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return result;
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
revenue.withPlugin(notificationPlugin);
|
|
565
518
|
```
|
|
566
519
|
|
|
567
|
-
###
|
|
520
|
+
### Resilience Patterns
|
|
521
|
+
|
|
522
|
+
Built-in retry, circuit breaker, and idempotency:
|
|
568
523
|
|
|
569
524
|
```typescript
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
525
|
+
// Automatic retry on provider failures
|
|
526
|
+
await revenue.payments.verify(transaction._id);
|
|
527
|
+
// Retries 3x with exponential backoff
|
|
528
|
+
|
|
529
|
+
// Manual idempotency
|
|
530
|
+
import { IdempotencyManager } from '@classytic/revenue';
|
|
531
|
+
|
|
532
|
+
const idem = new IdempotencyManager();
|
|
533
|
+
|
|
534
|
+
const result = await idem.execute(
|
|
535
|
+
'charge_user_123',
|
|
536
|
+
{ amount: 2999 },
|
|
537
|
+
() => revenue.monetization.create({ ... })
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
// Second call returns cached result (no duplicate charge)
|
|
583
541
|
```
|
|
584
542
|
|
|
585
|
-
###
|
|
543
|
+
### Money Utilities
|
|
544
|
+
|
|
545
|
+
No floating-point errors. All amounts in smallest currency unit (cents):
|
|
586
546
|
|
|
587
547
|
```typescript
|
|
588
|
-
import {
|
|
589
|
-
CurrentPaymentInputSchema,
|
|
590
|
-
PaymentEntrySchema,
|
|
591
|
-
validateSplitPayments,
|
|
592
|
-
safeValidate,
|
|
593
|
-
} from '@classytic/revenue';
|
|
548
|
+
import { Money, toSmallestUnit, fromSmallestUnit } from '@classytic/revenue';
|
|
594
549
|
|
|
595
|
-
//
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
console.log(result.error.issues); // "Split payments total must equal the transaction amount"
|
|
599
|
-
}
|
|
550
|
+
// Create Money instances
|
|
551
|
+
const price = Money.usd(1999); // $19.99
|
|
552
|
+
const euro = Money.of(2999, 'EUR'); // €29.99
|
|
600
553
|
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
554
|
+
// Conversions
|
|
555
|
+
toSmallestUnit(19.99, 'USD'); // 1999 cents
|
|
556
|
+
fromSmallestUnit(1999, 'USD'); // 19.99
|
|
557
|
+
|
|
558
|
+
// Arithmetic (immutable)
|
|
559
|
+
const total = price.add(Money.usd(500)); // $24.99
|
|
560
|
+
const discounted = price.multiply(0.9); // $17.99
|
|
561
|
+
|
|
562
|
+
// Fair allocation (handles rounding)
|
|
563
|
+
const [a, b, c] = Money.usd(100).allocate([1, 1, 1]);
|
|
564
|
+
// [34, 33, 33] cents - total = 100 ✓
|
|
565
|
+
|
|
566
|
+
// Formatting
|
|
567
|
+
price.format(); // "$19.99"
|
|
610
568
|
```
|
|
611
569
|
|
|
612
|
-
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## When to Use What
|
|
573
|
+
|
|
574
|
+
| Feature | Use Case |
|
|
575
|
+
|---------|----------|
|
|
576
|
+
| `monetization.create()` | New payment (subscription, purchase, free item) |
|
|
577
|
+
| `payments.verify()` | Mark payment successful after gateway confirmation |
|
|
578
|
+
| `payments.refund()` | Return money to customer (full or partial) |
|
|
579
|
+
| `escrow.hold()` | Marketplace - hold funds until delivery confirmed |
|
|
580
|
+
| `escrow.split()` | Affiliate/creator revenue sharing |
|
|
581
|
+
| Plugins | Tax calculation, logging, audit trails, metrics |
|
|
582
|
+
| Events | Send emails, grant/revoke access, analytics |
|
|
583
|
+
| State machines | Validate transitions, get allowed next actions |
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Real-World Example
|
|
588
|
+
|
|
589
|
+
**Course marketplace with affiliates:**
|
|
613
590
|
|
|
614
591
|
```typescript
|
|
615
|
-
|
|
592
|
+
// 1. Student buys course ($99)
|
|
593
|
+
const { transaction } = await revenue.monetization.create({
|
|
594
|
+
data: {
|
|
595
|
+
organizationId: 'org_123',
|
|
596
|
+
customerId: 'student_456',
|
|
597
|
+
sourceId: enrollmentId,
|
|
598
|
+
sourceModel: 'Enrollment',
|
|
599
|
+
},
|
|
600
|
+
planKey: 'one_time',
|
|
601
|
+
monetizationType: 'purchase',
|
|
602
|
+
entity: 'CourseEnrollment',
|
|
603
|
+
amount: 9900,
|
|
604
|
+
gateway: 'stripe',
|
|
605
|
+
});
|
|
616
606
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
amount: 30000,
|
|
620
|
-
reference: 'TRX456',
|
|
621
|
-
details: { walletNumber: '01712345678' },
|
|
622
|
-
};
|
|
607
|
+
// 2. Payment verified → Grant course access
|
|
608
|
+
await revenue.payments.verify(transaction._id);
|
|
623
609
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
610
|
+
// 3. Hold in escrow (30-day refund window)
|
|
611
|
+
await revenue.escrow.hold(transaction._id);
|
|
612
|
+
|
|
613
|
+
// 4. After 30 days, split revenue
|
|
614
|
+
await revenue.escrow.split(transaction._id, {
|
|
615
|
+
splits: [
|
|
616
|
+
{ recipientId: 'creator_123', percentage: 70 }, // $69.30
|
|
617
|
+
{ recipientId: 'affiliate_456', percentage: 10 }, // $9.90
|
|
618
|
+
],
|
|
619
|
+
organizationPercentage: 20, // $19.80 (platform)
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// 5. Calculate P&L
|
|
623
|
+
const income = await Transaction.aggregate([
|
|
624
|
+
{ $match: { flow: 'inflow', status: 'verified' } },
|
|
625
|
+
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
626
|
+
]);
|
|
630
627
|
```
|
|
631
628
|
|
|
632
629
|
---
|
|
633
630
|
|
|
634
|
-
##
|
|
631
|
+
## Submodule Imports
|
|
632
|
+
|
|
633
|
+
Tree-shakable imports for smaller bundles:
|
|
635
634
|
|
|
636
635
|
```typescript
|
|
637
|
-
|
|
638
|
-
import
|
|
636
|
+
// Plugins
|
|
637
|
+
import { loggingPlugin, auditPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
|
|
639
638
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
private stripe: Stripe;
|
|
639
|
+
// Enums
|
|
640
|
+
import { TRANSACTION_STATUS, PAYMENT_STATUS } from '@classytic/revenue/enums';
|
|
643
641
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
this.stripe = new Stripe(config.apiKey);
|
|
647
|
-
}
|
|
642
|
+
// Events
|
|
643
|
+
import { EventBus } from '@classytic/revenue/events';
|
|
648
644
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
amount: params.amount,
|
|
652
|
-
currency: params.currency ?? 'usd',
|
|
653
|
-
metadata: params.metadata,
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
return new PaymentIntent({
|
|
657
|
-
id: intent.id,
|
|
658
|
-
paymentIntentId: intent.id,
|
|
659
|
-
sessionId: null,
|
|
660
|
-
provider: this.name,
|
|
661
|
-
status: intent.status,
|
|
662
|
-
amount: intent.amount,
|
|
663
|
-
currency: intent.currency,
|
|
664
|
-
clientSecret: intent.client_secret!,
|
|
665
|
-
metadata: params.metadata ?? {},
|
|
666
|
-
});
|
|
667
|
-
}
|
|
645
|
+
// Schemas (Mongoose)
|
|
646
|
+
import { transactionSchema, subscriptionSchema } from '@classytic/revenue/schemas';
|
|
668
647
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return new PaymentResult({
|
|
672
|
-
id: intent.id,
|
|
673
|
-
provider: this.name,
|
|
674
|
-
status: intent.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
675
|
-
amount: intent.amount,
|
|
676
|
-
currency: intent.currency,
|
|
677
|
-
paidAt: intent.status === 'succeeded' ? new Date() : undefined,
|
|
678
|
-
metadata: {},
|
|
679
|
-
});
|
|
680
|
-
}
|
|
648
|
+
// Validation (Zod)
|
|
649
|
+
import { CreatePaymentSchema } from '@classytic/revenue/schemas/validation';
|
|
681
650
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async refund(paymentId: string, amount?: number | null): Promise<RefundResult> {
|
|
687
|
-
const refund = await this.stripe.refunds.create({
|
|
688
|
-
payment_intent: paymentId,
|
|
689
|
-
amount: amount ?? undefined,
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
return new RefundResult({
|
|
693
|
-
id: refund.id,
|
|
694
|
-
provider: this.name,
|
|
695
|
-
status: refund.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
696
|
-
amount: refund.amount,
|
|
697
|
-
currency: refund.currency,
|
|
698
|
-
refundedAt: new Date(),
|
|
699
|
-
metadata: {},
|
|
700
|
-
});
|
|
701
|
-
}
|
|
651
|
+
// Utilities
|
|
652
|
+
import { retry, calculateCommission } from '@classytic/revenue/utils';
|
|
702
653
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const event = this.stripe.webhooks.constructEvent(
|
|
706
|
-
payload as string,
|
|
707
|
-
sig!,
|
|
708
|
-
this.config.webhookSecret as string
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
return new WebhookEvent({
|
|
712
|
-
id: event.id,
|
|
713
|
-
provider: this.name,
|
|
714
|
-
type: event.type,
|
|
715
|
-
data: event.data.object as any,
|
|
716
|
-
createdAt: new Date(event.created * 1000),
|
|
717
|
-
});
|
|
718
|
-
}
|
|
654
|
+
// Reconciliation
|
|
655
|
+
import { reconcileSettlement } from '@classytic/revenue/reconciliation';
|
|
719
656
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
supportsWebhooks: true,
|
|
723
|
-
supportsRefunds: true,
|
|
724
|
-
supportsPartialRefunds: true,
|
|
725
|
-
requiresManualVerification: false,
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
}
|
|
657
|
+
// Services (advanced)
|
|
658
|
+
import { MonetizationService } from '@classytic/revenue/services';
|
|
729
659
|
```
|
|
730
660
|
|
|
731
661
|
---
|
|
732
662
|
|
|
663
|
+
## API Reference
|
|
664
|
+
|
|
665
|
+
### Services
|
|
666
|
+
|
|
667
|
+
| Service | Methods |
|
|
668
|
+
|---------|---------|
|
|
669
|
+
| `revenue.monetization` | `create()`, `renew()`, `cancel()`, `pause()`, `resume()` |
|
|
670
|
+
| `revenue.payments` | `verify()`, `refund()`, `getStatus()`, `handleWebhook()` |
|
|
671
|
+
| `revenue.transactions` | `get()`, `list()`, `update()` |
|
|
672
|
+
| `revenue.escrow` | `hold()`, `release()`, `cancel()`, `split()`, `getStatus()` |
|
|
673
|
+
| `revenue.settlement` | `createFromSplits()`, `processPending()`, `complete()`, `fail()`, `getSummary()` |
|
|
674
|
+
|
|
675
|
+
### State Machines
|
|
676
|
+
|
|
677
|
+
All state machines provide:
|
|
678
|
+
- `canTransition(from, to)` - Check if transition is valid
|
|
679
|
+
- `validate(from, to, id)` - Validate or throw error
|
|
680
|
+
- `getAllowedTransitions(state)` - Get next allowed states
|
|
681
|
+
- `isTerminalState(state)` - Check if state is final
|
|
682
|
+
|
|
683
|
+
### Utilities
|
|
684
|
+
|
|
685
|
+
| Function | Purpose |
|
|
686
|
+
|----------|---------|
|
|
687
|
+
| `calculateCommission(amount, rate, gatewayFee)` | Calculate platform commission |
|
|
688
|
+
| `calculateCommissionWithSplits(...)` | Commission with affiliate support |
|
|
689
|
+
| `reverseTax(originalTax, refundAmount)` | Proportional tax reversal |
|
|
690
|
+
| `retry(fn, options)` | Retry with exponential backoff |
|
|
691
|
+
| `reconcileSettlement(gatewayData, dbData)` | Gateway reconciliation |
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
733
695
|
## Error Handling
|
|
734
696
|
|
|
735
697
|
```typescript
|
|
736
698
|
import {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
699
|
+
PaymentIntentCreationError,
|
|
700
|
+
InvalidStateTransitionError,
|
|
701
|
+
InvalidAmountError,
|
|
740
702
|
RefundError,
|
|
741
|
-
ProviderNotFoundError,
|
|
742
|
-
ValidationError,
|
|
743
|
-
isRevenueError,
|
|
744
|
-
isRetryable,
|
|
745
703
|
} from '@classytic/revenue';
|
|
746
704
|
|
|
747
705
|
try {
|
|
748
|
-
await revenue.
|
|
706
|
+
await revenue.monetization.create({ amount: -100 }); // Invalid
|
|
749
707
|
} catch (error) {
|
|
750
|
-
if (error instanceof
|
|
751
|
-
console.
|
|
752
|
-
} else if (error instanceof
|
|
753
|
-
console.
|
|
754
|
-
} else if (isRevenueError(error) && isRetryable(error)) {
|
|
755
|
-
// Retry the operation
|
|
708
|
+
if (error instanceof InvalidAmountError) {
|
|
709
|
+
console.error('Amount must be positive');
|
|
710
|
+
} else if (error instanceof PaymentIntentCreationError) {
|
|
711
|
+
console.error('Payment gateway failed:', error.message);
|
|
756
712
|
}
|
|
757
713
|
}
|
|
714
|
+
|
|
715
|
+
// Or use Result type (no exceptions)
|
|
716
|
+
import { Result } from '@classytic/revenue';
|
|
717
|
+
|
|
718
|
+
const result = await revenue.execute(
|
|
719
|
+
() => revenue.payments.verify(txId),
|
|
720
|
+
{ idempotencyKey: 'verify_123' }
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
if (result.ok) {
|
|
724
|
+
console.log(result.value);
|
|
725
|
+
} else {
|
|
726
|
+
console.error(result.error);
|
|
727
|
+
}
|
|
758
728
|
```
|
|
759
729
|
|
|
760
730
|
---
|
|
761
731
|
|
|
762
|
-
## TypeScript
|
|
732
|
+
## TypeScript Support
|
|
763
733
|
|
|
764
|
-
Full
|
|
734
|
+
Full type safety with auto-completion:
|
|
765
735
|
|
|
766
736
|
```typescript
|
|
767
737
|
import type {
|
|
768
|
-
Revenue,
|
|
769
738
|
TransactionDocument,
|
|
770
739
|
SubscriptionDocument,
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
ProviderCapabilities,
|
|
774
|
-
RevenueEvents,
|
|
775
|
-
MonetizationCreateParams,
|
|
776
|
-
// Multi-payment types
|
|
777
|
-
PaymentEntry,
|
|
778
|
-
CurrentPayment,
|
|
779
|
-
PaymentEntryInput,
|
|
780
|
-
CurrentPaymentInput,
|
|
740
|
+
CommissionInfo,
|
|
741
|
+
RevenueConfig,
|
|
781
742
|
} from '@classytic/revenue';
|
|
743
|
+
|
|
744
|
+
const transaction: TransactionDocument = await revenue.transactions.get(txId);
|
|
745
|
+
const commission: CommissionInfo = transaction.commission;
|
|
782
746
|
```
|
|
783
747
|
|
|
784
|
-
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Examples
|
|
751
|
+
|
|
752
|
+
- [Quick Start](./examples/01-quick-start.ts) - Basic setup and first payment
|
|
753
|
+
- [Subscriptions](./examples/02-subscriptions.ts) - Recurring billing
|
|
754
|
+
- [Escrow & Splits](./examples/03-escrow-splits.ts) - Marketplace payouts
|
|
755
|
+
- [Events & Plugins](./examples/04-events-plugins.ts) - Extend functionality
|
|
756
|
+
- [Transaction Model](./examples/05-transaction-model.ts) - Complete model setup
|
|
757
|
+
- [Resilience Patterns](./examples/06-resilience.ts) - Retry, circuit breaker
|
|
758
|
+
|
|
759
|
+
---
|
|
785
760
|
|
|
786
|
-
|
|
761
|
+
## Built-in Plugins
|
|
787
762
|
|
|
788
763
|
```typescript
|
|
789
764
|
import {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
isHoldStatus,
|
|
796
|
-
isSplitType,
|
|
797
|
-
} from '@classytic/revenue';
|
|
765
|
+
loggingPlugin,
|
|
766
|
+
auditPlugin,
|
|
767
|
+
metricsPlugin,
|
|
768
|
+
createTaxPlugin
|
|
769
|
+
} from '@classytic/revenue/plugins';
|
|
798
770
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
771
|
+
revenue
|
|
772
|
+
.withPlugin(loggingPlugin({ level: 'info' }))
|
|
773
|
+
.withPlugin(auditPlugin({
|
|
774
|
+
store: async (entry) => {
|
|
775
|
+
await AuditLog.create(entry);
|
|
776
|
+
},
|
|
777
|
+
}))
|
|
778
|
+
.withPlugin(metricsPlugin({
|
|
779
|
+
onMetric: (metric) => {
|
|
780
|
+
statsd.timing(metric.name, metric.duration);
|
|
781
|
+
},
|
|
782
|
+
}))
|
|
783
|
+
.withPlugin(createTaxPlugin({ ... }));
|
|
812
784
|
```
|
|
813
785
|
|
|
814
|
-
**Available type guards:**
|
|
815
|
-
|
|
816
|
-
| Guard | Validates |
|
|
817
|
-
|-------|-----------|
|
|
818
|
-
| `isTransactionType` | `'income'` \| `'expense'` |
|
|
819
|
-
| `isTransactionStatus` | `'pending'` \| `'verified'` \| `'completed'` \| ... |
|
|
820
|
-
| `isLibraryCategory` | `'subscription'` \| `'purchase'` |
|
|
821
|
-
| `isPaymentStatus` | `'pending'` \| `'succeeded'` \| `'failed'` \| ... |
|
|
822
|
-
| `isPaymentGatewayType` | `'manual'` \| `'automatic'` |
|
|
823
|
-
| `isGatewayType` | `'redirect'` \| `'direct'` \| `'webhook'` |
|
|
824
|
-
| `isSubscriptionStatus` | `'active'` \| `'paused'` \| `'cancelled'` \| ... |
|
|
825
|
-
| `isPlanKey` | `'monthly'` \| `'yearly'` \| `'one_time'` \| ... |
|
|
826
|
-
| `isMonetizationType` | `'subscription'` \| `'purchase'` |
|
|
827
|
-
| `isHoldStatus` | `'held'` \| `'released'` \| `'partially_released'` \| ... |
|
|
828
|
-
| `isReleaseReason` | `'completed'` \| `'cancelled'` \| `'refunded'` \| ... |
|
|
829
|
-
| `isHoldReason` | `'escrow'` \| `'dispute'` \| `'verification'` \| ... |
|
|
830
|
-
| `isSplitType` | `'platform_commission'` \| `'affiliate_commission'` \| ... |
|
|
831
|
-
| `isSplitStatus` | `'pending'` \| `'processed'` \| `'failed'` |
|
|
832
|
-
| `isPayoutMethod` | `'bank_transfer'` \| `'wallet'` \| `'manual'` |
|
|
833
|
-
|
|
834
786
|
---
|
|
835
787
|
|
|
836
|
-
##
|
|
788
|
+
## Contributing
|
|
837
789
|
|
|
838
|
-
|
|
839
|
-
# Run all tests (196 tests)
|
|
840
|
-
npm test
|
|
790
|
+
Contributions welcome! Open an issue or submit a pull request on [GitHub](https://github.com/classytic/revenue).
|
|
841
791
|
|
|
842
|
-
|
|
843
|
-
npm test -- tests/integration/
|
|
792
|
+
---
|
|
844
793
|
|
|
845
|
-
|
|
846
|
-
npm run test:watch
|
|
794
|
+
## License
|
|
847
795
|
|
|
848
|
-
|
|
849
|
-
npm run test:coverage
|
|
850
|
-
```
|
|
796
|
+
MIT © [Classytic](https://github.com/classytic)
|
|
851
797
|
|
|
852
798
|
---
|
|
853
799
|
|
|
854
|
-
##
|
|
800
|
+
## Support
|
|
855
801
|
|
|
856
|
-
|
|
802
|
+
- 📖 [Documentation](https://github.com/classytic/revenue#readme)
|
|
803
|
+
- 🐛 [Issues](https://github.com/classytic/revenue/issues)
|
|
804
|
+
- 💬 [Discussions](https://github.com/classytic/revenue/discussions)
|