@classytic/revenue 1.0.2 → 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 +603 -486
- 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 +2361 -705
- package/dist/core/index.js.map +1 -1
- package/dist/enums/index.d.ts +54 -25
- package/dist/enums/index.js +143 -14
- package/dist/enums/index.js.map +1 -1
- package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
- package/dist/{index-BnJWVXuw.d.ts → index-DxIK0UmZ.d.ts} +281 -26
- package/dist/index-EnfKzDbs.d.ts +806 -0
- package/dist/{index-ChVD3P9k.d.ts → index-cLJBLUvx.d.ts} +55 -81
- package/dist/index.d.ts +16 -15
- package/dist/index.js +2583 -2066
- 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 +1927 -166
- package/dist/schemas/index.js +357 -40
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/validation.d.ts +87 -12
- package/dist/schemas/validation.js +71 -17
- 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 +370 -235
- package/dist/utils/index.js.map +1 -1
- package/package.json +27 -13
- package/dist/actions-CwG-b7fR.d.ts +0 -519
- package/dist/services/index.d.ts +0 -3
- package/dist/services/index.js +0 -1632
- package/dist/services/index.js.map +0 -1
- package/dist/split.enums-Bh24jw8p.d.ts +0 -255
- package/dist/split.schema-DYVP7Wu2.d.ts +0 -958
package/README.md
CHANGED
|
@@ -1,50 +1,197 @@
|
|
|
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
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@classytic/revenue)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What Is This?
|
|
14
|
+
|
|
15
|
+
A TypeScript library that handles **all financial transactions** in one unified model:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Subscription payment
|
|
19
|
+
{ type: 'subscription', flow: 'inflow', amount: 2999 }
|
|
20
|
+
|
|
21
|
+
// Product purchase
|
|
22
|
+
{ type: 'product_order', flow: 'inflow', amount: 1500 }
|
|
23
|
+
|
|
24
|
+
// Refund
|
|
25
|
+
{ type: 'refund', flow: 'outflow', amount: 1500 }
|
|
26
|
+
|
|
27
|
+
// Operational expense
|
|
28
|
+
{ type: 'rent', flow: 'outflow', amount: 50000 }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**One table. Query by type. Calculate P&L. Track cash flow.**
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
## Unified Cashflow Model (Shared Types)
|
|
35
|
+
|
|
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”.
|
|
37
|
+
|
|
38
|
+
Type safety is provided by `ITransaction` only. Transaction categories (`type`) are app-defined; `flow` (`inflow`/`outflow`) is the only shared enum.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import type { ITransaction } from '@classytic/shared-types';
|
|
42
|
+
// or: import type { ITransaction } from '@classytic/revenue';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Why Use This?
|
|
47
|
+
|
|
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
|
|
53
|
+
|
|
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
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## When to Use This
|
|
66
|
+
|
|
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 |
|
|
75
|
+
|
|
76
|
+
---
|
|
6
77
|
|
|
7
78
|
## Installation
|
|
8
79
|
|
|
9
80
|
```bash
|
|
10
|
-
npm install @classytic/revenue
|
|
81
|
+
npm install @classytic/revenue mongoose zod
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Peer Dependencies:**
|
|
85
|
+
- `mongoose` ^8.0.0 || ^9.0.0
|
|
86
|
+
- `zod` ^4.1.13
|
|
87
|
+
|
|
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
|
|
11
92
|
```
|
|
12
93
|
|
|
94
|
+
---
|
|
95
|
+
|
|
13
96
|
## Quick Start
|
|
14
97
|
|
|
15
|
-
###
|
|
98
|
+
### 1. Define Your Transaction Model
|
|
99
|
+
|
|
100
|
+
Copy the complete model from [examples/05-transaction-model.ts](./examples/05-transaction-model.ts):
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import mongoose, { Schema } from 'mongoose';
|
|
104
|
+
import type { ITransaction } from '@classytic/shared-types';
|
|
105
|
+
import {
|
|
106
|
+
TRANSACTION_FLOW_VALUES,
|
|
107
|
+
TRANSACTION_STATUS_VALUES,
|
|
108
|
+
gatewaySchema,
|
|
109
|
+
commissionSchema,
|
|
110
|
+
} from '@classytic/revenue';
|
|
111
|
+
|
|
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
|
+
};
|
|
121
|
+
|
|
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 });
|
|
137
|
+
|
|
138
|
+
export const Transaction = mongoose.model('Transaction', transactionSchema);
|
|
139
|
+
```
|
|
140
|
+
|
|
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
|
|
16
144
|
|
|
17
145
|
```typescript
|
|
18
|
-
import { Revenue
|
|
146
|
+
import { Revenue } from '@classytic/revenue';
|
|
19
147
|
import { ManualProvider } from '@classytic/revenue-manual';
|
|
20
148
|
|
|
21
|
-
const revenue = Revenue
|
|
22
|
-
|
|
23
|
-
.
|
|
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 })
|
|
24
155
|
.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
156
|
.build();
|
|
157
|
+
```
|
|
32
158
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
159
|
+
### 3. Create a Payment
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Create subscription payment
|
|
163
|
+
const { transaction, subscription } = await revenue.monetization.create({
|
|
164
|
+
data: {
|
|
165
|
+
organizationId: 'org_123',
|
|
166
|
+
customerId: 'user_456',
|
|
167
|
+
},
|
|
168
|
+
planKey: 'monthly',
|
|
169
|
+
monetizationType: 'subscription',
|
|
170
|
+
amount: 2999, // $29.99 in cents
|
|
171
|
+
gateway: 'manual',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
console.log(transaction.status); // 'pending'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 4. Verify Payment
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
await revenue.payments.verify(transaction._id);
|
|
181
|
+
|
|
182
|
+
// Transaction: 'pending' → 'verified'
|
|
183
|
+
// Subscription: 'pending' → 'active'
|
|
37
184
|
```
|
|
38
185
|
|
|
39
|
-
###
|
|
186
|
+
### 5. Handle Refunds
|
|
40
187
|
|
|
41
188
|
```typescript
|
|
42
|
-
|
|
189
|
+
// Full refund
|
|
190
|
+
await revenue.payments.refund(transaction._id);
|
|
43
191
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
options: { defaultCurrency: 'USD' },
|
|
192
|
+
// Partial refund: $10.00
|
|
193
|
+
await revenue.payments.refund(transaction._id, 1000, {
|
|
194
|
+
reason: 'customer_request',
|
|
48
195
|
});
|
|
49
196
|
```
|
|
50
197
|
|
|
@@ -52,567 +199,496 @@ const revenue = createRevenue({
|
|
|
52
199
|
|
|
53
200
|
## Core Concepts
|
|
54
201
|
|
|
55
|
-
###
|
|
202
|
+
### 1. Transaction Model (Required)
|
|
56
203
|
|
|
57
|
-
|
|
58
|
-
import { Money } from '@classytic/revenue';
|
|
204
|
+
**The universal ledger.** Every financial event becomes a transaction:
|
|
59
205
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
206
|
+
```typescript
|
|
207
|
+
// Query subscriptions
|
|
208
|
+
const subscriptions = await Transaction.find({
|
|
209
|
+
type: 'platform_subscription',
|
|
210
|
+
status: 'verified'
|
|
211
|
+
});
|
|
63
212
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
213
|
+
// Calculate revenue
|
|
214
|
+
const income = await Transaction.aggregate([
|
|
215
|
+
{ $match: { flow: 'inflow', status: 'verified' } },
|
|
216
|
+
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
217
|
+
]);
|
|
68
218
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
219
|
+
const expenses = await Transaction.aggregate([
|
|
220
|
+
{ $match: { flow: 'outflow', status: 'verified' } },
|
|
221
|
+
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
222
|
+
]);
|
|
73
223
|
|
|
74
|
-
|
|
75
|
-
const [a, b, c] = Money.usd(100).allocate([1, 1, 1]); // [34, 33, 33] cents
|
|
224
|
+
const netRevenue = income[0].total - expenses[0].total;
|
|
76
225
|
```
|
|
77
226
|
|
|
78
|
-
###
|
|
227
|
+
### 2. Payment Providers (Required)
|
|
228
|
+
|
|
229
|
+
**How money flows in.** Providers are swappable:
|
|
79
230
|
|
|
80
231
|
```typescript
|
|
81
|
-
import {
|
|
232
|
+
import { ManualProvider } from '@classytic/revenue-manual';
|
|
233
|
+
// import { StripeProvider } from '@classytic/revenue-stripe'; // Coming soon
|
|
82
234
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
(
|
|
86
|
-
{ idempotencyKey: 'order_123' }
|
|
87
|
-
);
|
|
235
|
+
revenue
|
|
236
|
+
.withProvider('manual', new ManualProvider())
|
|
237
|
+
.withProvider('stripe', new StripeProvider({ apiKey: '...' }));
|
|
88
238
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
239
|
+
// Use any provider
|
|
240
|
+
await revenue.monetization.create({
|
|
241
|
+
gateway: 'manual', // or 'stripe'
|
|
242
|
+
// ...
|
|
93
243
|
});
|
|
94
|
-
|
|
95
|
-
// Or simple check
|
|
96
|
-
if (result.ok) {
|
|
97
|
-
console.log(result.value);
|
|
98
|
-
} else {
|
|
99
|
-
console.log(result.error);
|
|
100
|
-
}
|
|
101
244
|
```
|
|
102
245
|
|
|
103
|
-
###
|
|
246
|
+
### 3. Plugins (Optional)
|
|
247
|
+
|
|
248
|
+
**Extend behavior.** Plugins add features without coupling:
|
|
104
249
|
|
|
105
250
|
```typescript
|
|
106
|
-
|
|
107
|
-
revenue.on('payment.succeeded', (event) => {
|
|
108
|
-
console.log('Transaction:', event.transactionId);
|
|
109
|
-
console.log('Amount:', event.transaction.amount);
|
|
110
|
-
});
|
|
251
|
+
import { loggingPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
|
|
111
252
|
|
|
112
|
-
revenue
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}));
|
|
262
|
+
```
|
|
115
263
|
|
|
116
|
-
|
|
117
|
-
console.log('Released:', event.releasedAmount);
|
|
118
|
-
});
|
|
264
|
+
---
|
|
119
265
|
|
|
120
|
-
|
|
121
|
-
revenue.on('*', (event) => {
|
|
122
|
-
analytics.track(event.type, event);
|
|
123
|
-
});
|
|
124
|
-
```
|
|
266
|
+
## Common Operations
|
|
125
267
|
|
|
126
|
-
###
|
|
268
|
+
### Create Subscription
|
|
127
269
|
|
|
128
270
|
```typescript
|
|
129
|
-
|
|
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',
|
|
280
|
+
});
|
|
130
281
|
|
|
131
|
-
//
|
|
132
|
-
|
|
282
|
+
// Later: Renew
|
|
283
|
+
await revenue.monetization.renew(subscription._id);
|
|
133
284
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
285
|
+
// Cancel
|
|
286
|
+
await revenue.monetization.cancel(subscription._id, {
|
|
287
|
+
reason: 'customer_requested',
|
|
288
|
+
});
|
|
139
289
|
```
|
|
140
290
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
## Services
|
|
144
|
-
|
|
145
|
-
### Monetization (Purchases & Subscriptions)
|
|
291
|
+
### Create One-Time Purchase
|
|
146
292
|
|
|
147
293
|
```typescript
|
|
148
|
-
|
|
149
|
-
const { transaction, paymentIntent } = await revenue.monetization.create({
|
|
294
|
+
const { transaction } = await revenue.monetization.create({
|
|
150
295
|
data: {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
296
|
+
organizationId: 'org_123',
|
|
297
|
+
customerId: 'user_456',
|
|
298
|
+
sourceId: order._id, // optional: stored as sourceId
|
|
299
|
+
sourceModel: 'Order', // optional: stored as sourceModel
|
|
155
300
|
},
|
|
156
301
|
planKey: 'one_time',
|
|
157
302
|
monetizationType: 'purchase',
|
|
158
|
-
amount:
|
|
303
|
+
amount: 10000, // $100.00
|
|
159
304
|
gateway: 'manual',
|
|
160
|
-
paymentData: { method: 'card' },
|
|
161
305
|
});
|
|
306
|
+
```
|
|
162
307
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
308
|
+
### Query Transactions
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// By type (category)
|
|
312
|
+
const subscriptions = await Transaction.find({
|
|
313
|
+
type: 'platform_subscription',
|
|
314
|
+
status: 'verified',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// By source (sourceId/sourceModel on the transaction)
|
|
318
|
+
const orderPayments = await Transaction.find({
|
|
319
|
+
sourceModel: 'Order',
|
|
320
|
+
sourceId: orderId,
|
|
170
321
|
});
|
|
171
322
|
|
|
172
|
-
//
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
await revenue.monetization.cancel(subscription._id, { immediate: true });
|
|
323
|
+
// By customer
|
|
324
|
+
const customerTransactions = await Transaction.find({
|
|
325
|
+
customerId: userId,
|
|
326
|
+
flow: 'inflow',
|
|
327
|
+
}).sort({ createdAt: -1 });
|
|
178
328
|
```
|
|
179
329
|
|
|
180
|
-
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Advanced Features
|
|
333
|
+
|
|
334
|
+
### State Machines (Data Integrity)
|
|
335
|
+
|
|
336
|
+
Prevent invalid transitions automatically:
|
|
181
337
|
|
|
182
338
|
```typescript
|
|
183
|
-
|
|
184
|
-
const { transaction, paymentResult } = await revenue.payments.verify(
|
|
185
|
-
transactionId,
|
|
186
|
-
{ verifiedBy: adminId }
|
|
187
|
-
);
|
|
339
|
+
import { TRANSACTION_STATE_MACHINE } from '@classytic/revenue';
|
|
188
340
|
|
|
189
|
-
//
|
|
190
|
-
|
|
341
|
+
// ✅ Valid
|
|
342
|
+
await revenue.payments.verify(transaction._id); // pending → verified
|
|
191
343
|
|
|
192
|
-
//
|
|
193
|
-
|
|
344
|
+
// ❌ Invalid (throws InvalidStateTransitionError)
|
|
345
|
+
await revenue.payments.verify(completedTransaction._id); // completed → verified
|
|
194
346
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
{ reason: 'Partial return' }
|
|
347
|
+
// Check if transition is valid
|
|
348
|
+
const canRefund = TRANSACTION_STATE_MACHINE.canTransition(
|
|
349
|
+
transaction.status,
|
|
350
|
+
'refunded'
|
|
200
351
|
);
|
|
201
352
|
|
|
202
|
-
//
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
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
|
|
359
|
+
```
|
|
360
|
+
|
|
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
|
|
367
|
+
|
|
368
|
+
### Audit Trail (Track State Changes)
|
|
369
|
+
|
|
370
|
+
Every state transition is automatically logged:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
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
|
+
// ]
|
|
208
389
|
```
|
|
209
390
|
|
|
210
|
-
### Escrow (
|
|
391
|
+
### Escrow (Marketplaces)
|
|
392
|
+
|
|
393
|
+
Hold funds until conditions met:
|
|
211
394
|
|
|
212
395
|
```typescript
|
|
213
|
-
//
|
|
214
|
-
await revenue.
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
404
|
});
|
|
218
405
|
|
|
219
|
-
// Release to
|
|
220
|
-
await revenue.escrow.release(
|
|
221
|
-
recipientId:
|
|
406
|
+
// Release to seller after delivery confirmed
|
|
407
|
+
await revenue.escrow.release(transaction._id, {
|
|
408
|
+
recipientId: 'seller_123',
|
|
222
409
|
recipientType: 'organization',
|
|
223
|
-
|
|
410
|
+
reason: 'delivery_confirmed',
|
|
224
411
|
});
|
|
412
|
+
```
|
|
225
413
|
|
|
226
|
-
|
|
227
|
-
await revenue.escrow.split(transactionId, [
|
|
228
|
-
{ type: 'platform_commission', recipientId: 'platform', rate: 0.10 },
|
|
229
|
-
{ type: 'affiliate_commission', recipientId: 'aff_123', rate: 0.05 },
|
|
230
|
-
]);
|
|
414
|
+
### Commission Splits (Affiliates)
|
|
231
415
|
|
|
232
|
-
|
|
233
|
-
await revenue.escrow.cancelHold(transactionId, { reason: 'Order cancelled' });
|
|
234
|
-
```
|
|
416
|
+
Split revenue between multiple parties:
|
|
235
417
|
|
|
236
|
-
|
|
418
|
+
```typescript
|
|
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%
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Creates 3 transactions:
|
|
428
|
+
// - Creator: $70.00
|
|
429
|
+
// - Affiliate: $10.00
|
|
430
|
+
// - Platform: $20.00
|
|
431
|
+
```
|
|
237
432
|
|
|
238
|
-
|
|
433
|
+
### Events (React to Changes)
|
|
239
434
|
|
|
240
435
|
```typescript
|
|
241
|
-
import {
|
|
436
|
+
import { EventBus } from '@classytic/revenue/events';
|
|
242
437
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
.withPlugin(loggingPlugin({ level: 'info' }))
|
|
247
|
-
.withPlugin(auditPlugin({ store: saveToDatabase }))
|
|
248
|
-
.withPlugin(metricsPlugin({ onMetric: sendToDatadog }))
|
|
249
|
-
.build();
|
|
438
|
+
revenue.events.on('payment:verified', async (event) => {
|
|
439
|
+
// Grant access
|
|
440
|
+
await grantAccess(event.transaction.customerId);
|
|
250
441
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
name: 'rate-limit',
|
|
254
|
-
hooks: {
|
|
255
|
-
'payment.create.before': async (ctx, input, next) => {
|
|
256
|
-
if (await isRateLimited(input.customerId)) {
|
|
257
|
-
throw new Error('Rate limited');
|
|
258
|
-
}
|
|
259
|
-
return next();
|
|
260
|
-
},
|
|
261
|
-
},
|
|
442
|
+
// Send email
|
|
443
|
+
await sendEmail(event.transaction.customerId, 'Payment received!');
|
|
262
444
|
});
|
|
263
|
-
```
|
|
264
445
|
|
|
265
|
-
|
|
446
|
+
revenue.events.on('subscription:cancelled', async (event) => {
|
|
447
|
+
await removeAccess(event.subscription.customerId);
|
|
448
|
+
});
|
|
266
449
|
|
|
267
|
-
|
|
450
|
+
// Other events:
|
|
451
|
+
// - monetization:created, payment:failed, payment:refunded
|
|
452
|
+
// - subscription:activated, subscription:renewed
|
|
453
|
+
// - escrow:held, escrow:released, settlement:completed
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Tax Plugin (Optional)
|
|
268
457
|
|
|
269
|
-
|
|
458
|
+
Automatically calculate and track tax:
|
|
270
459
|
|
|
271
460
|
```typescript
|
|
272
|
-
import {
|
|
273
|
-
|
|
274
|
-
// Simple retry
|
|
275
|
-
const data = await retry(
|
|
276
|
-
() => fetchPaymentStatus(id),
|
|
277
|
-
{
|
|
278
|
-
maxAttempts: 5,
|
|
279
|
-
baseDelay: 1000,
|
|
280
|
-
maxDelay: 30000,
|
|
281
|
-
backoffMultiplier: 2,
|
|
282
|
-
jitter: 0.1,
|
|
283
|
-
}
|
|
284
|
-
);
|
|
461
|
+
import { createTaxPlugin } from '@classytic/revenue/plugins';
|
|
285
462
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
479
|
+
// ...
|
|
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);
|
|
291
492
|
```
|
|
292
493
|
|
|
293
|
-
###
|
|
494
|
+
### Custom Plugins
|
|
294
495
|
|
|
295
496
|
```typescript
|
|
296
|
-
import {
|
|
497
|
+
import { definePlugin } from '@classytic/revenue/plugins';
|
|
297
498
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
});
|
|
302
511
|
|
|
303
|
-
|
|
512
|
+
return result;
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
});
|
|
304
516
|
|
|
305
|
-
|
|
306
|
-
console.log(breaker.getState()); // 'closed' | 'open' | 'half-open'
|
|
517
|
+
revenue.withPlugin(notificationPlugin);
|
|
307
518
|
```
|
|
308
519
|
|
|
309
|
-
###
|
|
520
|
+
### Resilience Patterns
|
|
521
|
+
|
|
522
|
+
Built-in retry, circuit breaker, and idempotency:
|
|
310
523
|
|
|
311
524
|
```typescript
|
|
525
|
+
// Automatic retry on provider failures
|
|
526
|
+
await revenue.payments.verify(transaction._id);
|
|
527
|
+
// Retries 3x with exponential backoff
|
|
528
|
+
|
|
529
|
+
// Manual idempotency
|
|
312
530
|
import { IdempotencyManager } from '@classytic/revenue';
|
|
313
531
|
|
|
314
|
-
const
|
|
532
|
+
const idem = new IdempotencyManager();
|
|
315
533
|
|
|
316
|
-
const result = await
|
|
317
|
-
'
|
|
318
|
-
{ amount:
|
|
319
|
-
() =>
|
|
534
|
+
const result = await idem.execute(
|
|
535
|
+
'charge_user_123',
|
|
536
|
+
{ amount: 2999 },
|
|
537
|
+
() => revenue.monetization.create({ ... })
|
|
320
538
|
);
|
|
321
539
|
|
|
322
|
-
//
|
|
323
|
-
// Same key + different params = error
|
|
540
|
+
// Second call returns cached result (no duplicate charge)
|
|
324
541
|
```
|
|
325
542
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
## Transaction Model Setup
|
|
329
|
-
|
|
330
|
-
**ONE Transaction model = Universal Financial Ledger**
|
|
543
|
+
### Money Utilities
|
|
331
544
|
|
|
332
|
-
|
|
545
|
+
No floating-point errors. All amounts in smallest currency unit (cents):
|
|
333
546
|
|
|
334
547
|
```typescript
|
|
335
|
-
import
|
|
336
|
-
import {
|
|
337
|
-
// Enums
|
|
338
|
-
TRANSACTION_TYPE_VALUES,
|
|
339
|
-
TRANSACTION_STATUS_VALUES,
|
|
340
|
-
// Mongoose schemas (compose into your model)
|
|
341
|
-
gatewaySchema,
|
|
342
|
-
paymentDetailsSchema,
|
|
343
|
-
commissionSchema,
|
|
344
|
-
holdSchema,
|
|
345
|
-
splitSchema,
|
|
346
|
-
} from '@classytic/revenue';
|
|
548
|
+
import { Money, toSmallestUnit, fromSmallestUnit } from '@classytic/revenue';
|
|
347
549
|
|
|
348
|
-
//
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
'course_enrollment',
|
|
352
|
-
'product_order',
|
|
353
|
-
'refund',
|
|
354
|
-
'rent',
|
|
355
|
-
'salary',
|
|
356
|
-
'utilities',
|
|
357
|
-
];
|
|
358
|
-
|
|
359
|
-
const transactionSchema = new mongoose.Schema({
|
|
360
|
-
// Core fields
|
|
361
|
-
organizationId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
|
|
362
|
-
customerId: { type: mongoose.Schema.Types.ObjectId, index: true },
|
|
363
|
-
type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true }, // income | expense
|
|
364
|
-
category: { type: String, enum: CATEGORIES, index: true },
|
|
365
|
-
status: { type: String, enum: TRANSACTION_STATUS_VALUES, default: 'pending' },
|
|
366
|
-
amount: { type: Number, required: true, min: 0 },
|
|
367
|
-
currency: { type: String, default: 'USD' },
|
|
368
|
-
method: { type: String, required: true },
|
|
550
|
+
// Create Money instances
|
|
551
|
+
const price = Money.usd(1999); // $19.99
|
|
552
|
+
const euro = Money.of(2999, 'EUR'); // €29.99
|
|
369
553
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
paymentDetails: paymentDetailsSchema,
|
|
374
|
-
hold: holdSchema,
|
|
375
|
-
splits: [splitSchema],
|
|
376
|
-
|
|
377
|
-
// Polymorphic reference (link to any entity)
|
|
378
|
-
referenceId: { type: mongoose.Schema.Types.ObjectId, refPath: 'referenceModel' },
|
|
379
|
-
referenceModel: { type: String, enum: ['Subscription', 'Order', 'Enrollment'] },
|
|
380
|
-
|
|
381
|
-
// Idempotency & verification
|
|
382
|
-
idempotencyKey: { type: String, unique: true, sparse: true },
|
|
383
|
-
verifiedAt: Date,
|
|
384
|
-
verifiedBy: mongoose.Schema.Types.Mixed, // ObjectId or 'system'
|
|
385
|
-
|
|
386
|
-
// Refunds
|
|
387
|
-
refundedAmount: Number,
|
|
388
|
-
refundedAt: Date,
|
|
389
|
-
|
|
390
|
-
metadata: mongoose.Schema.Types.Mixed,
|
|
391
|
-
}, { timestamps: true });
|
|
554
|
+
// Conversions
|
|
555
|
+
toSmallestUnit(19.99, 'USD'); // 1999 cents
|
|
556
|
+
fromSmallestUnit(1999, 'USD'); // 19.99
|
|
392
557
|
|
|
393
|
-
|
|
394
|
-
|
|
558
|
+
// Arithmetic (immutable)
|
|
559
|
+
const total = price.add(Money.usd(500)); // $24.99
|
|
560
|
+
const discounted = price.multiply(0.9); // $17.99
|
|
395
561
|
|
|
396
|
-
|
|
562
|
+
// Fair allocation (handles rounding)
|
|
563
|
+
const [a, b, c] = Money.usd(100).allocate([1, 1, 1]);
|
|
564
|
+
// [34, 33, 33] cents - total = 100 ✓
|
|
397
565
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
| `commissionSchema` | Platform commission | `commission: commissionSchema` |
|
|
402
|
-
| `paymentDetailsSchema` | Manual payment info | `paymentDetails: paymentDetailsSchema` |
|
|
403
|
-
| `holdSchema` | Escrow hold/release | `hold: holdSchema` |
|
|
404
|
-
| `splitSchema` | Multi-party splits | `splits: [splitSchema]` |
|
|
405
|
-
| `currentPaymentSchema` | For Order/Subscription models | `currentPayment: currentPaymentSchema` |
|
|
566
|
+
// Formatting
|
|
567
|
+
price.format(); // "$19.99"
|
|
568
|
+
```
|
|
406
569
|
|
|
407
|
-
|
|
570
|
+
---
|
|
408
571
|
|
|
409
|
-
|
|
410
|
-
import { gatewaySchema, commissionSchema } from '@classytic/revenue';
|
|
572
|
+
## When to Use What
|
|
411
573
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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 |
|
|
418
584
|
|
|
419
585
|
---
|
|
420
586
|
|
|
421
|
-
##
|
|
587
|
+
## Real-World Example
|
|
422
588
|
|
|
423
|
-
|
|
589
|
+
**Course marketplace with affiliates:**
|
|
424
590
|
|
|
425
591
|
```typescript
|
|
426
|
-
//
|
|
427
|
-
const
|
|
428
|
-
const orderTotal = 10000;
|
|
429
|
-
|
|
430
|
-
// Friend 1 pays $40
|
|
431
|
-
await revenue.monetization.create({
|
|
592
|
+
// 1. Student buys course ($99)
|
|
593
|
+
const { transaction } = await revenue.monetization.create({
|
|
432
594
|
data: {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
595
|
+
organizationId: 'org_123',
|
|
596
|
+
customerId: 'student_456',
|
|
597
|
+
sourceId: enrollmentId,
|
|
598
|
+
sourceModel: 'Enrollment',
|
|
437
599
|
},
|
|
438
|
-
planKey: '
|
|
600
|
+
planKey: 'one_time',
|
|
439
601
|
monetizationType: 'purchase',
|
|
440
|
-
|
|
602
|
+
entity: 'CourseEnrollment',
|
|
603
|
+
amount: 9900,
|
|
441
604
|
gateway: 'stripe',
|
|
442
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
443
605
|
});
|
|
444
606
|
|
|
445
|
-
//
|
|
446
|
-
await revenue.
|
|
447
|
-
data: {
|
|
448
|
-
customerId: friend2,
|
|
449
|
-
organizationId: restaurantId,
|
|
450
|
-
referenceId: orderId,
|
|
451
|
-
referenceModel: 'Order',
|
|
452
|
-
},
|
|
453
|
-
planKey: 'split_payment',
|
|
454
|
-
monetizationType: 'purchase',
|
|
455
|
-
amount: 3500,
|
|
456
|
-
gateway: 'stripe',
|
|
457
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
458
|
-
});
|
|
607
|
+
// 2. Payment verified → Grant course access
|
|
608
|
+
await revenue.payments.verify(transaction._id);
|
|
459
609
|
|
|
460
|
-
//
|
|
461
|
-
await revenue.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
amount: 2500,
|
|
471
|
-
gateway: 'stripe',
|
|
472
|
-
metadata: { splitGroup: 'dinner_dec_10' },
|
|
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)
|
|
473
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
|
+
]);
|
|
474
627
|
```
|
|
475
628
|
|
|
476
|
-
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## Submodule Imports
|
|
632
|
+
|
|
633
|
+
Tree-shakable imports for smaller bundles:
|
|
477
634
|
|
|
478
635
|
```typescript
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
referenceId: orderId,
|
|
482
|
-
referenceModel: 'Order',
|
|
483
|
-
});
|
|
636
|
+
// Plugins
|
|
637
|
+
import { loggingPlugin, auditPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
|
|
484
638
|
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
const totalPaid = verified.reduce((sum, t) => sum + t.amount, 0);
|
|
488
|
-
const remaining = orderTotal - totalPaid;
|
|
489
|
-
const isFullyPaid = totalPaid >= orderTotal;
|
|
490
|
-
|
|
491
|
-
console.log({
|
|
492
|
-
totalPaid, // 10000
|
|
493
|
-
remaining, // 0
|
|
494
|
-
isFullyPaid, // true
|
|
495
|
-
payers: verified.map(t => ({
|
|
496
|
-
customerId: t.customerId,
|
|
497
|
-
amount: t.amount,
|
|
498
|
-
paidAt: t.verifiedAt,
|
|
499
|
-
})),
|
|
500
|
-
});
|
|
501
|
-
```
|
|
639
|
+
// Enums
|
|
640
|
+
import { TRANSACTION_STATUS, PAYMENT_STATUS } from '@classytic/revenue/enums';
|
|
502
641
|
|
|
503
|
-
|
|
642
|
+
// Events
|
|
643
|
+
import { EventBus } from '@classytic/revenue/events';
|
|
504
644
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const groupPayments = await Transaction.find({
|
|
508
|
-
'metadata.splitGroup': 'dinner_dec_10',
|
|
509
|
-
});
|
|
645
|
+
// Schemas (Mongoose)
|
|
646
|
+
import { transactionSchema, subscriptionSchema } from '@classytic/revenue/schemas';
|
|
510
647
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
referenceId: orderId,
|
|
514
|
-
status: 'pending',
|
|
515
|
-
});
|
|
516
|
-
```
|
|
648
|
+
// Validation (Zod)
|
|
649
|
+
import { CreatePaymentSchema } from '@classytic/revenue/schemas/validation';
|
|
517
650
|
|
|
518
|
-
|
|
651
|
+
// Utilities
|
|
652
|
+
import { retry, calculateCommission } from '@classytic/revenue/utils';
|
|
519
653
|
|
|
520
|
-
|
|
654
|
+
// Reconciliation
|
|
655
|
+
import { reconcileSettlement } from '@classytic/revenue/reconciliation';
|
|
521
656
|
|
|
522
|
-
|
|
523
|
-
import {
|
|
524
|
-
|
|
657
|
+
// Services (advanced)
|
|
658
|
+
import { MonetizationService } from '@classytic/revenue/services';
|
|
659
|
+
```
|
|
525
660
|
|
|
526
|
-
|
|
527
|
-
public override readonly name = 'stripe';
|
|
528
|
-
private stripe: Stripe;
|
|
661
|
+
---
|
|
529
662
|
|
|
530
|
-
|
|
531
|
-
super(config);
|
|
532
|
-
this.stripe = new Stripe(config.apiKey);
|
|
533
|
-
}
|
|
663
|
+
## API Reference
|
|
534
664
|
|
|
535
|
-
|
|
536
|
-
const intent = await this.stripe.paymentIntents.create({
|
|
537
|
-
amount: params.amount,
|
|
538
|
-
currency: params.currency ?? 'usd',
|
|
539
|
-
metadata: params.metadata,
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
return new PaymentIntent({
|
|
543
|
-
id: intent.id,
|
|
544
|
-
paymentIntentId: intent.id,
|
|
545
|
-
sessionId: null,
|
|
546
|
-
provider: this.name,
|
|
547
|
-
status: intent.status,
|
|
548
|
-
amount: intent.amount,
|
|
549
|
-
currency: intent.currency,
|
|
550
|
-
clientSecret: intent.client_secret!,
|
|
551
|
-
metadata: params.metadata ?? {},
|
|
552
|
-
});
|
|
553
|
-
}
|
|
665
|
+
### Services
|
|
554
666
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
currency: intent.currency,
|
|
563
|
-
paidAt: intent.status === 'succeeded' ? new Date() : undefined,
|
|
564
|
-
metadata: {},
|
|
565
|
-
});
|
|
566
|
-
}
|
|
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()` |
|
|
567
674
|
|
|
568
|
-
|
|
569
|
-
return this.verifyPayment(intentId);
|
|
570
|
-
}
|
|
675
|
+
### State Machines
|
|
571
676
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return new RefundResult({
|
|
579
|
-
id: refund.id,
|
|
580
|
-
provider: this.name,
|
|
581
|
-
status: refund.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
582
|
-
amount: refund.amount,
|
|
583
|
-
currency: refund.currency,
|
|
584
|
-
refundedAt: new Date(),
|
|
585
|
-
metadata: {},
|
|
586
|
-
});
|
|
587
|
-
}
|
|
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
|
|
588
682
|
|
|
589
|
-
|
|
590
|
-
const sig = headers?.['stripe-signature'];
|
|
591
|
-
const event = this.stripe.webhooks.constructEvent(
|
|
592
|
-
payload as string,
|
|
593
|
-
sig!,
|
|
594
|
-
this.config.webhookSecret as string
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
return new WebhookEvent({
|
|
598
|
-
id: event.id,
|
|
599
|
-
provider: this.name,
|
|
600
|
-
type: event.type,
|
|
601
|
-
data: event.data.object as any,
|
|
602
|
-
createdAt: new Date(event.created * 1000),
|
|
603
|
-
});
|
|
604
|
-
}
|
|
683
|
+
### Utilities
|
|
605
684
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
```
|
|
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 |
|
|
616
692
|
|
|
617
693
|
---
|
|
618
694
|
|
|
@@ -620,68 +696,109 @@ export class StripeProvider extends PaymentProvider {
|
|
|
620
696
|
|
|
621
697
|
```typescript
|
|
622
698
|
import {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
699
|
+
PaymentIntentCreationError,
|
|
700
|
+
InvalidStateTransitionError,
|
|
701
|
+
InvalidAmountError,
|
|
626
702
|
RefundError,
|
|
627
|
-
ProviderNotFoundError,
|
|
628
|
-
ValidationError,
|
|
629
|
-
isRevenueError,
|
|
630
|
-
isRetryable,
|
|
631
703
|
} from '@classytic/revenue';
|
|
632
704
|
|
|
633
705
|
try {
|
|
634
|
-
await revenue.
|
|
706
|
+
await revenue.monetization.create({ amount: -100 }); // Invalid
|
|
635
707
|
} catch (error) {
|
|
636
|
-
if (error instanceof
|
|
637
|
-
console.
|
|
638
|
-
} else if (error instanceof
|
|
639
|
-
console.
|
|
640
|
-
} else if (isRevenueError(error) && isRetryable(error)) {
|
|
641
|
-
// 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);
|
|
642
712
|
}
|
|
643
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
|
+
}
|
|
644
728
|
```
|
|
645
729
|
|
|
646
730
|
---
|
|
647
731
|
|
|
648
|
-
## TypeScript
|
|
732
|
+
## TypeScript Support
|
|
649
733
|
|
|
650
|
-
Full
|
|
734
|
+
Full type safety with auto-completion:
|
|
651
735
|
|
|
652
736
|
```typescript
|
|
653
737
|
import type {
|
|
654
|
-
Revenue,
|
|
655
738
|
TransactionDocument,
|
|
656
739
|
SubscriptionDocument,
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
ProviderCapabilities,
|
|
660
|
-
RevenueEvents,
|
|
661
|
-
MonetizationCreateParams,
|
|
740
|
+
CommissionInfo,
|
|
741
|
+
RevenueConfig,
|
|
662
742
|
} from '@classytic/revenue';
|
|
743
|
+
|
|
744
|
+
const transaction: TransactionDocument = await revenue.transactions.get(txId);
|
|
745
|
+
const commission: CommissionInfo = transaction.commission;
|
|
663
746
|
```
|
|
664
747
|
|
|
665
748
|
---
|
|
666
749
|
|
|
667
|
-
##
|
|
750
|
+
## Examples
|
|
668
751
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
|
672
758
|
|
|
673
|
-
|
|
674
|
-
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Built-in Plugins
|
|
675
762
|
|
|
676
|
-
|
|
677
|
-
|
|
763
|
+
```typescript
|
|
764
|
+
import {
|
|
765
|
+
loggingPlugin,
|
|
766
|
+
auditPlugin,
|
|
767
|
+
metricsPlugin,
|
|
768
|
+
createTaxPlugin
|
|
769
|
+
} from '@classytic/revenue/plugins';
|
|
678
770
|
|
|
679
|
-
|
|
680
|
-
|
|
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({ ... }));
|
|
681
784
|
```
|
|
682
785
|
|
|
683
786
|
---
|
|
684
787
|
|
|
788
|
+
## Contributing
|
|
789
|
+
|
|
790
|
+
Contributions welcome! Open an issue or submit a pull request on [GitHub](https://github.com/classytic/revenue).
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
685
794
|
## License
|
|
686
795
|
|
|
687
796
|
MIT © [Classytic](https://github.com/classytic)
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## Support
|
|
801
|
+
|
|
802
|
+
- 📖 [Documentation](https://github.com/classytic/revenue#readme)
|
|
803
|
+
- 🐛 [Issues](https://github.com/classytic/revenue/issues)
|
|
804
|
+
- 💬 [Discussions](https://github.com/classytic/revenue/discussions)
|