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