@classytic/revenue 1.1.4 → 2.0.0
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/CHANGELOG.md +90 -0
- package/README.md +638 -632
- package/dist/audit-B39B0Sdq.mjs +53 -0
- package/dist/audit-DZ0eTr9g.d.mts +89 -0
- package/dist/bridges/index.d.mts +2 -0
- package/dist/bridges/index.mjs +1 -0
- package/dist/context-DRqSeTPM.d.mts +35 -0
- package/dist/core/state-machines.d.mts +35 -0
- package/dist/core/state-machines.mjs +134 -0
- package/dist/engine-types-CcjIb4Fy.d.mts +611 -0
- package/dist/enums/index.d.mts +3 -157
- package/dist/enums/index.mjs +3 -55
- package/dist/errors-DHa8JVQ-.mjs +92 -0
- package/dist/escrow.schema-BBv9oVEW.mjs +322 -0
- package/dist/escrow.schema-CC8XuD46.d.mts +629 -0
- package/dist/event-constants-CEMitnIV.mjs +53 -0
- package/dist/events/index.d.mts +3 -0
- package/dist/events/index.mjs +4 -0
- package/dist/index.d.mts +77 -9
- package/dist/index.mjs +465 -29
- package/dist/monetization.enums-BtiU3t8o.mjs +39 -0
- package/dist/monetization.enums-D2xbxXJM.d.mts +34 -0
- package/dist/plugins/plugin.interface.d.mts +28 -0
- package/dist/plugins/plugin.interface.mjs +26 -0
- package/dist/providers/index.d.mts +2 -3
- package/dist/providers/index.mjs +2 -2
- package/dist/{base-DCoyIUj6.mjs → registry-DhFMsSn5.mjs} +34 -36
- package/dist/{base-CsTlVQJe.d.mts → registry-SvIGPAx_.d.mts} +73 -66
- package/dist/repositories/create-repositories.d.mts +21 -0
- package/dist/repositories/create-repositories.mjs +12 -0
- package/dist/revenue-bridges-sdlrR85c.d.mts +145 -0
- package/dist/revenue-event-catalog-BX3g7RUi.d.mts +823 -0
- package/dist/revenue-event-catalog-LqxPnsU_.mjs +388 -0
- package/dist/settlement.repository-Cy3mMWGH.mjs +771 -0
- package/dist/shared/index.d.mts +2 -0
- package/dist/shared/index.mjs +4 -0
- package/dist/split.enums-CQE3ekH1.mjs +172 -0
- package/dist/split.enums-Dw4zCrcZ.d.mts +154 -0
- package/dist/splits-BAfY-a9P.mjs +123 -0
- package/dist/validators/index.d.mts +2 -0
- package/dist/validators/index.mjs +3 -0
- package/package.json +33 -37
- package/dist/application/services/index.d.mts +0 -4
- package/dist/application/services/index.mjs +0 -3
- package/dist/category-resolver-DV83N8ok.mjs +0 -284
- package/dist/commission-split-BzB8cd39.mjs +0 -485
- package/dist/core/events.d.mts +0 -294
- package/dist/core/events.mjs +0 -100
- package/dist/core/index.d.mts +0 -9
- package/dist/core/index.mjs +0 -8
- package/dist/errors-rRdOqnWx.d.mts +0 -787
- package/dist/escrow.enums-CZGrrdg7.mjs +0 -101
- package/dist/escrow.enums-DwdLuuve.d.mts +0 -78
- package/dist/idempotency-DaYcUGY1.mjs +0 -172
- package/dist/index-Dsp7H5Wb.d.mts +0 -471
- package/dist/infrastructure/plugins/index.d.mts +0 -239
- package/dist/infrastructure/plugins/index.mjs +0 -345
- package/dist/money-CvrDOijQ.mjs +0 -271
- package/dist/money-DPG8AtJ8.d.mts +0 -112
- package/dist/payment.enums-HAuAS9Pp.d.mts +0 -70
- package/dist/payment.enums-tEFVa-Xp.mjs +0 -69
- package/dist/plugin-BbK0OVHy.d.mts +0 -327
- package/dist/plugin-Cd_V04Em.mjs +0 -210
- package/dist/reconciliation/index.d.mts +0 -193
- package/dist/reconciliation/index.mjs +0 -192
- package/dist/retry-HHCOXYdn.d.mts +0 -186
- package/dist/revenue-BhdS7nXh.mjs +0 -553
- package/dist/schemas/index.d.mts +0 -2665
- package/dist/schemas/index.mjs +0 -717
- package/dist/schemas/validation.d.mts +0 -375
- package/dist/schemas/validation.mjs +0 -325
- package/dist/settlement.enums-DFhkqZEY.d.mts +0 -132
- package/dist/settlement.schema-DnNSFpGd.d.mts +0 -344
- package/dist/settlement.service-DjzAjezU.d.mts +0 -594
- package/dist/settlement.service-DmdKv0Zu.mjs +0 -2511
- package/dist/split.enums-BrjabxIX.mjs +0 -86
- package/dist/split.enums-DmskfLOM.d.mts +0 -43
- package/dist/tax-BoCt5cEd.d.mts +0 -61
- package/dist/tax-EQ15DO81.mjs +0 -162
- package/dist/transaction.enums-pCyMFT4Z.mjs +0 -96
- package/dist/utils/index.d.mts +0 -428
- package/dist/utils/index.mjs +0 -346
package/README.md
CHANGED
|
@@ -1,805 +1,811 @@
|
|
|
1
|
-
# @classytic/revenue
|
|
1
|
+
# @classytic/revenue v2
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Payment lifecycle engine — transactions, subscriptions, escrow, settlements, commissions.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@classytic/revenue)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
MongoKit repositories with domain verbs. Arc-compatible event transport. No service layer — repositories ARE the API.
|
|
10
6
|
|
|
11
7
|
---
|
|
12
8
|
|
|
13
|
-
##
|
|
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 }
|
|
9
|
+
## Install
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Refund
|
|
25
|
-
{ type: 'refund', flow: 'outflow', amount: 1500 }
|
|
26
|
-
|
|
27
|
-
// Operational expense
|
|
28
|
-
{ type: 'rent', flow: 'outflow', amount: 50000 }
|
|
11
|
+
```bash
|
|
12
|
+
npm install @classytic/revenue @classytic/mongokit mongoose zod
|
|
13
|
+
npm install @classytic/revenue-manual # built-in manual provider
|
|
29
14
|
```
|
|
30
15
|
|
|
31
|
-
|
|
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.
|
|
16
|
+
## Quick Start
|
|
39
17
|
|
|
40
18
|
```typescript
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
|
|
19
|
+
import { createRevenue } from '@classytic/revenue';
|
|
20
|
+
import { ManualProvider } from '@classytic/revenue-manual';
|
|
45
21
|
|
|
46
|
-
|
|
22
|
+
const revenue = await createRevenue({
|
|
23
|
+
connection: mongoose.connection,
|
|
24
|
+
defaultCurrency: 'BDT',
|
|
25
|
+
providers: { manual: new ManualProvider() },
|
|
26
|
+
});
|
|
47
27
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
// Create payment — returns raw mongokit doc
|
|
29
|
+
const txn = await revenue.repositories.transaction.createPaymentIntent({
|
|
30
|
+
amount: 10000,
|
|
31
|
+
gateway: 'manual',
|
|
32
|
+
data: { customerId: 'cust_1', sourceId: 'order_1', sourceModel: 'Order' },
|
|
33
|
+
});
|
|
34
|
+
// txn.publicId → 'txn_a7b3xk9m2p1q4d5e6f'
|
|
35
|
+
// txn.gateway.metadata.instructions → 'Payment Amount: 10000 BDT...'
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- ✅ **Type-safe** = Full TypeScript + Zod v4 validation
|
|
61
|
-
- ✅ **Integer money** = No floating-point errors
|
|
37
|
+
// Verify (admin approves manual payment)
|
|
38
|
+
const verified = await revenue.repositories.transaction.verify(
|
|
39
|
+
txn.gateway.paymentIntentId,
|
|
40
|
+
{ verifiedBy: 'admin_1' },
|
|
41
|
+
);
|
|
42
|
+
// verified.status → 'verified'
|
|
62
43
|
|
|
63
|
-
|
|
44
|
+
// Refund — returns the refund transaction doc
|
|
45
|
+
const refundTxn = await revenue.repositories.transaction.refund(
|
|
46
|
+
txn._id.toString(), 5000, { reason: 'partial return' },
|
|
47
|
+
);
|
|
48
|
+
// refundTxn.type → 'refund', refundTxn.flow → 'outflow', refundTxn.amount → 5000
|
|
49
|
+
```
|
|
64
50
|
|
|
65
|
-
##
|
|
51
|
+
## Architecture
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
53
|
+
```
|
|
54
|
+
createRevenue(config) --> RevenueEngine
|
|
55
|
+
|
|
|
56
|
+
|-- repositories.transaction extends mongokit Repository
|
|
57
|
+
| getAll, getById, getByQuery, create, update, delete, count (inherited)
|
|
58
|
+
| createPaymentIntent, verify, refund, handleWebhook (domain verbs)
|
|
59
|
+
| hold, release, split (escrow verbs)
|
|
60
|
+
|
|
|
61
|
+
|-- repositories.subscription extends mongokit Repository
|
|
62
|
+
| getAll, getById, create, update, delete, count (inherited)
|
|
63
|
+
| activate, cancel, pause, resume (domain verbs)
|
|
64
|
+
|
|
|
65
|
+
|-- repositories.settlement extends mongokit Repository
|
|
66
|
+
| getAll, getById, create, update, delete, count (inherited)
|
|
67
|
+
| schedule, processPending, complete, fail (domain verbs)
|
|
68
|
+
|
|
|
69
|
+
|-- providers ProviderRegistry
|
|
70
|
+
|-- events RevenueEventTransport (Arc-compatible)
|
|
71
|
+
|-- models Mongoose models (for Arc adapter)
|
|
72
|
+
```
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
Repositories extend mongokit `Repository`. CRUD + pagination + query is inherited. Domain verbs contain real business logic (state machine transitions, provider calls, event emission). No service layer. No proxy methods.
|
|
77
75
|
|
|
78
|
-
##
|
|
76
|
+
## RevenueConfig
|
|
79
77
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
```typescript
|
|
79
|
+
const revenue = await createRevenue({
|
|
80
|
+
// Required
|
|
81
|
+
connection: mongoose.connection,
|
|
82
|
+
defaultCurrency: 'BDT',
|
|
83
|
+
|
|
84
|
+
// Providers — register any payment gateway
|
|
85
|
+
providers: {
|
|
86
|
+
manual: new ManualProvider(),
|
|
87
|
+
stripe: new StripeProvider({ apiKey: '...' }),
|
|
88
|
+
bkash: new BkashProvider({ ... }),
|
|
89
|
+
},
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Modules — progressive opt-in
|
|
92
|
+
modules: {
|
|
93
|
+
subscription: true, // default: true
|
|
94
|
+
escrow: true, // default: false
|
|
95
|
+
settlement: true, // default: false
|
|
96
|
+
commission: { // commission calculation
|
|
97
|
+
defaultRate: 0.05,
|
|
98
|
+
gatewayFeeRate: 0.025,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
// Event transport — Arc-compatible, drop-in Redis/Outbox
|
|
103
|
+
eventTransport: new RedisEventTransport(ioredis),
|
|
104
|
+
|
|
105
|
+
// Bridges — optional external integrations
|
|
106
|
+
bridges: {
|
|
107
|
+
ledger: { onPaymentVerified: async (txn, ctx) => { ... } },
|
|
108
|
+
tax: { computeTax: async (amount, taxClass, ctx) => { ... } },
|
|
109
|
+
notification: { onPaymentVerified: async (txn, ctx) => { ... } },
|
|
110
|
+
currency: { convert: async (amount, from, to) => { ... } },
|
|
111
|
+
customer: { getCustomer: async (id) => { ... } },
|
|
112
|
+
analytics: { trackEvent: async (name, payload) => { ... } },
|
|
113
|
+
},
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
// MongoKit plugins — inject per repository
|
|
116
|
+
repositoryPlugins: {
|
|
117
|
+
transaction: [cachePlugin({ adapter: redis })],
|
|
118
|
+
},
|
|
96
119
|
|
|
97
|
-
|
|
120
|
+
// Schema extensions — add custom fields to models
|
|
121
|
+
schemaOptions: {
|
|
122
|
+
transaction: {
|
|
123
|
+
extraFields: { branch: { type: String }, vatInvoiceNumber: { type: String } },
|
|
124
|
+
extraIndexes: [{ fields: { branch: 1, createdAt: -1 } }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
multiTenant: true, // default: true
|
|
129
|
+
});
|
|
130
|
+
```
|
|
100
131
|
|
|
101
|
-
|
|
132
|
+
## RevenueEngine
|
|
102
133
|
|
|
103
134
|
```typescript
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
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
|
-
};
|
|
122
|
-
|
|
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 });
|
|
138
|
-
|
|
139
|
-
export const Transaction = mongoose.model('Transaction', transactionSchema);
|
|
135
|
+
interface RevenueEngine {
|
|
136
|
+
config: Readonly<RevenueConfig>;
|
|
137
|
+
models: RevenueModels; // Mongoose models
|
|
138
|
+
repositories: RevenueRepositories; // MongoKit repositories (the API surface)
|
|
139
|
+
providers: ProviderRegistry; // Payment providers
|
|
140
|
+
events: RevenueEventTransport; // Event transport
|
|
141
|
+
destroy(): Promise<void>;
|
|
142
|
+
}
|
|
140
143
|
```
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
### 2. Initialize Revenue
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
import { Revenue } from '@classytic/revenue';
|
|
148
|
-
import { ManualProvider } from '@classytic/revenue-manual';
|
|
145
|
+
---
|
|
149
146
|
|
|
150
|
-
|
|
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
|
-
```
|
|
147
|
+
## Arc Integration
|
|
159
148
|
|
|
160
|
-
|
|
149
|
+
Arc auto-generates CRUD routes from mongokit repositories. State transitions go through Arc's **Action Router** (Stripe pattern) — one endpoint per resource, action name in body.
|
|
161
150
|
|
|
162
151
|
```typescript
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
import { defineResource } from '@classytic/arc';
|
|
153
|
+
import { requireRoles } from '@classytic/arc/permissions';
|
|
154
|
+
import { createAdapter } from '#shared/adapter';
|
|
155
|
+
|
|
156
|
+
export default defineResource({
|
|
157
|
+
name: 'transaction',
|
|
158
|
+
prefix: '/revenue/transactions',
|
|
159
|
+
adapter: createAdapter(revenue.models.Transaction, revenue.repositories.transaction),
|
|
160
|
+
presets: ['multiTenant', 'softDelete'],
|
|
161
|
+
|
|
162
|
+
// State transitions → unified action endpoint POST /:id/action
|
|
163
|
+
actions: {
|
|
164
|
+
verify: {
|
|
165
|
+
handler: (id, data, req) => revenue.repositories.transaction.verify(id, data, req.scope),
|
|
166
|
+
permissions: requireRoles('admin', 'finance-manager'),
|
|
167
|
+
schema: { verifiedBy: { type: 'string' } },
|
|
168
|
+
description: 'Verify a pending payment',
|
|
169
|
+
},
|
|
170
|
+
refund: {
|
|
171
|
+
handler: (id, data, req) =>
|
|
172
|
+
revenue.repositories.transaction.refund(id, data.amount, { reason: data.reason }, req.scope),
|
|
173
|
+
permissions: requireRoles('admin'),
|
|
174
|
+
schema: {
|
|
175
|
+
amount: { type: 'number', minimum: 1 },
|
|
176
|
+
reason: { type: 'string', minLength: 3 },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
hold: {
|
|
180
|
+
handler: (id, data, req) => revenue.repositories.transaction.hold(id, data, req.scope),
|
|
181
|
+
permissions: requireRoles('admin', 'marketplace-ops'),
|
|
182
|
+
schema: { reason: { type: 'string' }, amount: { type: 'number' } },
|
|
183
|
+
},
|
|
184
|
+
release: {
|
|
185
|
+
handler: (id, data, req) => revenue.repositories.transaction.release(id, data, req.scope),
|
|
186
|
+
permissions: requireRoles('admin', 'marketplace-ops'),
|
|
187
|
+
schema: {
|
|
188
|
+
recipientId: { type: 'string' },
|
|
189
|
+
recipientType: { type: 'string' },
|
|
190
|
+
amount: { type: 'number' },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
split: {
|
|
194
|
+
handler: (id, data, req) => revenue.repositories.transaction.split(id, data.rules, req.scope),
|
|
195
|
+
permissions: requireRoles('admin'),
|
|
196
|
+
schema: { rules: { type: 'array' } },
|
|
197
|
+
},
|
|
168
198
|
},
|
|
169
|
-
planKey: 'monthly',
|
|
170
|
-
monetizationType: 'subscription',
|
|
171
|
-
amount: 2999, // $29.99 in cents
|
|
172
|
-
gateway: 'manual',
|
|
173
|
-
});
|
|
174
199
|
|
|
175
|
-
|
|
200
|
+
// Non-state transitions stay as custom routes (webhooks, queries, batch ops)
|
|
201
|
+
routes: [
|
|
202
|
+
{
|
|
203
|
+
method: 'POST', path: '/webhook/:provider',
|
|
204
|
+
handler: (req) =>
|
|
205
|
+
revenue.repositories.transaction.handleWebhook(req.params.provider, req.body, req.headers),
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
});
|
|
176
209
|
```
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
**Generated endpoints:**
|
|
212
|
+
```
|
|
213
|
+
GET /revenue/transactions ← list (QueryParser filters)
|
|
214
|
+
GET /revenue/transactions/:id ← get single
|
|
215
|
+
PATCH /revenue/transactions/:id ← raw update (gate with permissions)
|
|
216
|
+
DELETE /revenue/transactions/:id ← soft delete
|
|
217
|
+
POST /revenue/transactions/:id/action ← verify | refund | hold | release | split
|
|
218
|
+
POST /revenue/transactions/webhook/:provider ← provider webhooks
|
|
185
219
|
```
|
|
186
220
|
|
|
187
|
-
|
|
188
|
-
|
|
221
|
+
**Frontend usage:**
|
|
189
222
|
```typescript
|
|
190
|
-
//
|
|
191
|
-
await revenue
|
|
223
|
+
// State transition via action endpoint
|
|
224
|
+
await fetch('/revenue/transactions/txn_abc123/action', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
body: JSON.stringify({ action: 'verify', verifiedBy: 'admin_1' }),
|
|
227
|
+
});
|
|
192
228
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
reason: '
|
|
229
|
+
await fetch('/revenue/transactions/txn_abc123/action', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
body: JSON.stringify({ action: 'refund', amount: 5000, reason: 'customer request' }),
|
|
196
232
|
});
|
|
233
|
+
|
|
234
|
+
// Filter list via QueryParser
|
|
235
|
+
await fetch('/revenue/transactions?status=verified&amount_gte=1000&sort=-createdAt&page=1&limit=20');
|
|
197
236
|
```
|
|
198
237
|
|
|
238
|
+
**Why actions instead of one endpoint per verb:** ~40% fewer routes, single audit point, self-documenting via OpenAPI action enum, type-safe action validation, per-action permissions and schemas. State machine validation lives inside the repository domain verb — `STATE_MACHINE.validate(from, to, id)` throws `InvalidStateTransitionError` if the transition is illegal.
|
|
239
|
+
|
|
199
240
|
---
|
|
200
241
|
|
|
201
|
-
##
|
|
242
|
+
## Building a Custom Provider
|
|
202
243
|
|
|
203
|
-
|
|
244
|
+
Every payment gateway implements the `PaymentProvider` abstract class. See `@classytic/revenue-manual` as the reference implementation.
|
|
204
245
|
|
|
205
|
-
|
|
246
|
+
### PaymentProvider Interface
|
|
206
247
|
|
|
207
248
|
```typescript
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
type: 'platform_subscription',
|
|
211
|
-
status: 'verified'
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Calculate revenue
|
|
215
|
-
const income = await Transaction.aggregate([
|
|
216
|
-
{ $match: { flow: 'inflow', status: 'verified' } },
|
|
217
|
-
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
const expenses = await Transaction.aggregate([
|
|
221
|
-
{ $match: { flow: 'outflow', status: 'verified' } },
|
|
222
|
-
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
|
223
|
-
]);
|
|
224
|
-
|
|
225
|
-
const netRevenue = income[0].total - expenses[0].total;
|
|
226
|
-
```
|
|
249
|
+
import { PaymentProvider, PaymentIntent, PaymentResult, RefundResult, WebhookEvent } from '@classytic/revenue';
|
|
250
|
+
import type { CreateIntentParams, ProviderCapabilities } from '@classytic/revenue/providers';
|
|
227
251
|
|
|
228
|
-
|
|
252
|
+
export class StripeProvider extends PaymentProvider {
|
|
253
|
+
public override readonly name = 'stripe';
|
|
229
254
|
|
|
230
|
-
|
|
255
|
+
constructor(config: { apiKey: string }) {
|
|
256
|
+
super(config);
|
|
257
|
+
}
|
|
231
258
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
259
|
+
// 1. Create payment intent — called by createPaymentIntent()
|
|
260
|
+
async createIntent(params: CreateIntentParams): Promise<PaymentIntent> {
|
|
261
|
+
const stripe = new Stripe(this.config.apiKey as string);
|
|
262
|
+
const intent = await stripe.paymentIntents.create({
|
|
263
|
+
amount: params.amount,
|
|
264
|
+
currency: params.currency,
|
|
265
|
+
metadata: params.metadata as Stripe.MetadataParam,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return new PaymentIntent({
|
|
269
|
+
id: intent.id,
|
|
270
|
+
sessionId: null,
|
|
271
|
+
paymentIntentId: intent.id,
|
|
272
|
+
provider: 'stripe',
|
|
273
|
+
status: intent.status,
|
|
274
|
+
amount: intent.amount,
|
|
275
|
+
currency: intent.currency,
|
|
276
|
+
clientSecret: intent.client_secret, // frontend needs this
|
|
277
|
+
metadata: params.metadata ?? {},
|
|
278
|
+
raw: intent,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
235
281
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
282
|
+
// 2. Verify payment — called by verify()
|
|
283
|
+
async verifyPayment(intentId: string): Promise<PaymentResult> {
|
|
284
|
+
const stripe = new Stripe(this.config.apiKey as string);
|
|
285
|
+
const intent = await stripe.paymentIntents.retrieve(intentId);
|
|
286
|
+
|
|
287
|
+
return new PaymentResult({
|
|
288
|
+
id: intent.id,
|
|
289
|
+
provider: 'stripe',
|
|
290
|
+
status: intent.status === 'succeeded' ? 'succeeded'
|
|
291
|
+
: intent.status === 'requires_action' ? 'requires_action'
|
|
292
|
+
: intent.status === 'processing' ? 'processing'
|
|
293
|
+
: 'failed',
|
|
294
|
+
amount: intent.amount,
|
|
295
|
+
currency: intent.currency,
|
|
296
|
+
paidAt: intent.status === 'succeeded' ? new Date() : undefined,
|
|
297
|
+
metadata: {},
|
|
298
|
+
raw: intent,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
239
301
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
245
|
-
```
|
|
302
|
+
// 3. Get status — same as verify for most providers
|
|
303
|
+
async getStatus(intentId: string): Promise<PaymentResult> {
|
|
304
|
+
return this.verifyPayment(intentId);
|
|
305
|
+
}
|
|
246
306
|
|
|
247
|
-
|
|
307
|
+
// 4. Refund — called by refund()
|
|
308
|
+
async refund(paymentId: string, amount?: number | null, options?: { reason?: string }): Promise<RefundResult> {
|
|
309
|
+
const stripe = new Stripe(this.config.apiKey as string);
|
|
310
|
+
const refund = await stripe.refunds.create({
|
|
311
|
+
payment_intent: paymentId,
|
|
312
|
+
amount: amount ?? undefined,
|
|
313
|
+
reason: options?.reason as any,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return new RefundResult({
|
|
317
|
+
id: refund.id,
|
|
318
|
+
provider: 'stripe',
|
|
319
|
+
status: refund.status === 'succeeded' ? 'succeeded' : 'processing',
|
|
320
|
+
amount: refund.amount,
|
|
321
|
+
currency: refund.currency,
|
|
322
|
+
refundedAt: new Date(),
|
|
323
|
+
reason: options?.reason,
|
|
324
|
+
metadata: {},
|
|
325
|
+
raw: refund,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
248
328
|
|
|
249
|
-
|
|
329
|
+
// 5. Handle webhook — called by handleWebhook()
|
|
330
|
+
async handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent> {
|
|
331
|
+
const stripe = new Stripe(this.config.apiKey as string);
|
|
332
|
+
const sig = headers?.['stripe-signature'] ?? '';
|
|
333
|
+
const event = stripe.webhooks.constructEvent(payload as string, sig, this.config.webhookSecret as string);
|
|
334
|
+
|
|
335
|
+
return new WebhookEvent({
|
|
336
|
+
id: event.id,
|
|
337
|
+
provider: 'stripe',
|
|
338
|
+
type: event.type,
|
|
339
|
+
data: {
|
|
340
|
+
paymentIntentId: (event.data.object as any).id,
|
|
341
|
+
sessionId: (event.data.object as any).id,
|
|
342
|
+
},
|
|
343
|
+
createdAt: new Date(event.created * 1000),
|
|
344
|
+
raw: event,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
250
347
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}),
|
|
262
|
-
}));
|
|
348
|
+
// 6. Capabilities — tells revenue what this provider supports
|
|
349
|
+
override getCapabilities(): ProviderCapabilities {
|
|
350
|
+
return {
|
|
351
|
+
supportsWebhooks: true,
|
|
352
|
+
supportsRefunds: true,
|
|
353
|
+
supportsPartialRefunds: true,
|
|
354
|
+
requiresManualVerification: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
263
358
|
```
|
|
264
359
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
## Common Operations
|
|
268
|
-
|
|
269
|
-
### Create Subscription
|
|
360
|
+
### Required Methods
|
|
270
361
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
amount: 2999, // $29.99/month
|
|
280
|
-
gateway: 'manual',
|
|
281
|
-
});
|
|
362
|
+
| Method | Called By | Returns | Purpose |
|
|
363
|
+
|---|---|---|---|
|
|
364
|
+
| `createIntent(params)` | `repo.createPaymentIntent()` | `PaymentIntent` | Initialize payment with gateway |
|
|
365
|
+
| `verifyPayment(intentId)` | `repo.verify()` | `PaymentResult` | Check payment status with gateway |
|
|
366
|
+
| `getStatus(intentId)` | Direct call | `PaymentResult` | Poll payment status |
|
|
367
|
+
| `refund(paymentId, amount?, options?)` | `repo.refund()` | `RefundResult` | Process refund with gateway |
|
|
368
|
+
| `handleWebhook(payload, headers?)` | `repo.handleWebhook()` | `WebhookEvent` | Parse incoming webhook |
|
|
369
|
+
| `getCapabilities()` | Engine | `ProviderCapabilities` | Declare supported features |
|
|
282
370
|
|
|
283
|
-
|
|
284
|
-
await revenue.monetization.renew(subscription._id);
|
|
371
|
+
### PaymentResult Status Map
|
|
285
372
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
373
|
+
| Provider Status | Maps To | Revenue Action |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| `'succeeded'` | `TRANSACTION_STATUS.VERIFIED` | Mark verified, call ledger bridge |
|
|
376
|
+
| `'failed'` | `TRANSACTION_STATUS.FAILED` | Mark failed |
|
|
377
|
+
| `'processing'` | `TRANSACTION_STATUS.PROCESSING` | Wait for webhook |
|
|
378
|
+
| `'requires_action'` | `TRANSACTION_STATUS.REQUIRES_ACTION` | Return to frontend for 3DS/OTP |
|
|
291
379
|
|
|
292
|
-
###
|
|
380
|
+
### Register Provider
|
|
293
381
|
|
|
294
382
|
```typescript
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
383
|
+
const revenue = await createRevenue({
|
|
384
|
+
connection,
|
|
385
|
+
defaultCurrency: 'BDT',
|
|
386
|
+
providers: {
|
|
387
|
+
manual: new ManualProvider(),
|
|
388
|
+
stripe: new StripeProvider({ apiKey: process.env.STRIPE_KEY }),
|
|
389
|
+
bkash: new BkashProvider({ appKey: '...', appSecret: '...' }),
|
|
301
390
|
},
|
|
302
|
-
planKey: 'one_time',
|
|
303
|
-
monetizationType: 'purchase',
|
|
304
|
-
amount: 10000, // $100.00
|
|
305
|
-
gateway: 'manual',
|
|
306
391
|
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Query Transactions
|
|
310
392
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
status: 'verified',
|
|
393
|
+
// Use by gateway name
|
|
394
|
+
const txn = await revenue.repositories.transaction.createPaymentIntent({
|
|
395
|
+
amount: 5000,
|
|
396
|
+
gateway: 'bkash', // matches key in providers map
|
|
316
397
|
});
|
|
317
|
-
|
|
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 });
|
|
329
398
|
```
|
|
330
399
|
|
|
331
400
|
---
|
|
332
401
|
|
|
333
|
-
##
|
|
402
|
+
## Event System
|
|
334
403
|
|
|
335
|
-
|
|
404
|
+
Revenue uses `RevenueEventTransport` — a structural superset of Arc's `DomainEvent`. Any Arc transport drops in with zero adapters.
|
|
336
405
|
|
|
337
|
-
|
|
406
|
+
### RevenueDomainEvent
|
|
338
407
|
|
|
339
408
|
```typescript
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// Check if state is terminal
|
|
359
|
-
const isDone = TRANSACTION_STATE_MACHINE.isTerminalState('refunded'); // true
|
|
409
|
+
interface RevenueDomainEvent<T = unknown> {
|
|
410
|
+
type: string; // 'revenue:payment.verified'
|
|
411
|
+
payload: T; // event-specific data
|
|
412
|
+
meta: {
|
|
413
|
+
id: string; // crypto.randomUUID()
|
|
414
|
+
timestamp: Date;
|
|
415
|
+
resource?: string; // 'transaction', 'subscription', 'settlement'
|
|
416
|
+
resourceId?: string; // publicId (txn_..., sub_..., stl_...)
|
|
417
|
+
userId?: string; // from RevenueContext.actorId
|
|
418
|
+
organizationId?: string; // from RevenueContext.organizationId
|
|
419
|
+
correlationId?: string; // from RevenueContext.traceId
|
|
420
|
+
aggregate?: string; // 'revenue'
|
|
421
|
+
version?: number;
|
|
422
|
+
causationId?: string;
|
|
423
|
+
tags?: string[];
|
|
424
|
+
};
|
|
425
|
+
}
|
|
360
426
|
```
|
|
361
427
|
|
|
362
|
-
|
|
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
|
|
368
|
-
|
|
369
|
-
### Audit Trail (Track State Changes)
|
|
370
|
-
|
|
371
|
-
Every state transition is automatically logged:
|
|
428
|
+
### RevenueEventTransport
|
|
372
429
|
|
|
373
430
|
```typescript
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
// ]
|
|
431
|
+
interface RevenueEventTransport {
|
|
432
|
+
publish(event: RevenueDomainEvent): Promise<void>;
|
|
433
|
+
subscribe?(pattern: string, handler: RevenueEventHandler): Promise<() => void>;
|
|
434
|
+
close?(): Promise<void>;
|
|
435
|
+
}
|
|
390
436
|
```
|
|
391
437
|
|
|
392
|
-
###
|
|
393
|
-
|
|
394
|
-
Hold funds until conditions met:
|
|
438
|
+
### Drop-in Transports
|
|
395
439
|
|
|
396
440
|
```typescript
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
await
|
|
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
|
|
441
|
+
// Arc Redis
|
|
442
|
+
import { RedisEventTransport } from '@classytic/arc/events';
|
|
443
|
+
const revenue = await createRevenue({
|
|
444
|
+
eventTransport: new RedisEventTransport(ioredis),
|
|
405
445
|
});
|
|
406
446
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
// Arc Outbox (guaranteed delivery)
|
|
448
|
+
import { EventOutbox } from '@classytic/arc/events';
|
|
449
|
+
const outbox = new EventOutbox({ store: mongoOutboxStore, transport: redisTransport });
|
|
450
|
+
const revenue = await createRevenue({
|
|
451
|
+
eventTransport: { publish: (event) => outbox.store(event) },
|
|
412
452
|
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Commission Splits (Affiliates)
|
|
416
|
-
|
|
417
|
-
Split revenue between multiple parties:
|
|
418
453
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
{ recipientId: 'affiliate_456', recipientType: 'user', percentage: 10 },
|
|
424
|
-
],
|
|
425
|
-
organizationPercentage: 20, // Platform keeps 20%
|
|
454
|
+
// No events (testing)
|
|
455
|
+
import { NoopRevenueEventTransport } from '@classytic/revenue';
|
|
456
|
+
const revenue = await createRevenue({
|
|
457
|
+
eventTransport: new NoopRevenueEventTransport(),
|
|
426
458
|
});
|
|
427
|
-
|
|
428
|
-
// Creates 3 transactions:
|
|
429
|
-
// - Creator: $70.00
|
|
430
|
-
// - Affiliate: $10.00
|
|
431
|
-
// - Platform: $20.00
|
|
432
459
|
```
|
|
433
460
|
|
|
434
|
-
###
|
|
461
|
+
### Pattern Matching
|
|
435
462
|
|
|
436
463
|
```typescript
|
|
437
|
-
|
|
464
|
+
await revenue.events.subscribe?.('revenue:payment.*', handler); // payment.verified, payment.refunded, ...
|
|
465
|
+
await revenue.events.subscribe?.('revenue:*', handler); // all revenue events
|
|
466
|
+
await revenue.events.subscribe?.('*', handler); // everything
|
|
467
|
+
await revenue.events.subscribe?.('revenue:escrow.held', handler); // exact match
|
|
468
|
+
```
|
|
438
469
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
470
|
+
### Event Reference
|
|
471
|
+
|
|
472
|
+
| Event | Payload |
|
|
473
|
+
|---|---|
|
|
474
|
+
| `revenue:monetization.created` | `{ monetizationType, transaction }` |
|
|
475
|
+
| `revenue:payment.verified` | `{ transaction, paymentResult, verifiedBy }` |
|
|
476
|
+
| `revenue:payment.failed` | `{ transaction, paymentResult }` |
|
|
477
|
+
| `revenue:payment.refunded` | `{ transaction, refundTransaction, refundAmount, reason }` |
|
|
478
|
+
| `revenue:payment.requires_action` | `{ transaction, paymentResult }` |
|
|
479
|
+
| `revenue:payment.processing` | `{ transaction, paymentResult }` |
|
|
480
|
+
| `revenue:subscription.activated` | `{ subscription, activatedAt }` |
|
|
481
|
+
| `revenue:subscription.cancelled` | `{ subscription, immediate, reason }` |
|
|
482
|
+
| `revenue:subscription.paused` | `{ subscription, reason }` |
|
|
483
|
+
| `revenue:subscription.resumed` | `{ subscription, extendPeriod }` |
|
|
484
|
+
| `revenue:escrow.held` | `{ transaction, heldAmount, reason }` |
|
|
485
|
+
| `revenue:escrow.released` | `{ transaction, releaseAmount, recipientId, isFullRelease }` |
|
|
486
|
+
| `revenue:escrow.split` | `{ transaction, splits, organizationPayout }` |
|
|
487
|
+
| `revenue:settlement.scheduled` | `{ settlement, scheduledAt }` |
|
|
488
|
+
| `revenue:settlement.processing` | `{ settlement, processedAt }` |
|
|
489
|
+
| `revenue:settlement.completed` | `{ settlement, completedAt }` |
|
|
490
|
+
| `revenue:settlement.failed` | `{ settlement, reason, retry }` |
|
|
491
|
+
| `revenue:webhook.processed` | `{ webhookType, provider, transaction }` |
|
|
442
492
|
|
|
443
|
-
|
|
444
|
-
await sendEmail(event.transaction.customerId, 'Payment received!');
|
|
445
|
-
});
|
|
493
|
+
---
|
|
446
494
|
|
|
447
|
-
|
|
448
|
-
await removeAccess(event.subscription.customerId);
|
|
449
|
-
});
|
|
495
|
+
## Bridges
|
|
450
496
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
497
|
+
All bridges are optional. Every method is optional. Features degrade gracefully when bridge is absent.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
interface RevenueBridges {
|
|
501
|
+
ledger?: LedgerBridge; // post journal entries on payment events
|
|
502
|
+
tax?: TaxBridge; // compute tax for amounts
|
|
503
|
+
notification?: NotificationBridge; // send emails/SMS on lifecycle events
|
|
504
|
+
currency?: CurrencyBridge; // multi-currency conversion
|
|
505
|
+
customer?: CustomerBridge; // resolve customer details
|
|
506
|
+
analytics?: AnalyticsBridge; // track events for BI
|
|
507
|
+
source?: SourceBridge; // resolve polymorphic source documents (Order, Invoice, Stripe charge, etc.)
|
|
508
|
+
}
|
|
455
509
|
```
|
|
456
510
|
|
|
457
|
-
###
|
|
511
|
+
### SourceBridge — Polymorphic Source Resolution
|
|
458
512
|
|
|
459
|
-
|
|
513
|
+
Revenue stores `sourceId` as a `String` so it works with any ID format: ObjectId hex, UUIDs, Stripe IDs, REST API resource IDs. Hosts implement `SourceBridge` to teach revenue how to load source documents — works for any deployment topology.
|
|
460
514
|
|
|
461
515
|
```typescript
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}),
|
|
474
|
-
}))
|
|
475
|
-
.build();
|
|
476
|
-
|
|
477
|
-
// Tax calculated automatically
|
|
478
|
-
const { transaction } = await revenue.monetization.create({
|
|
479
|
-
amount: 10000, // $100.00
|
|
480
|
-
// ...
|
|
516
|
+
// Same MongoDB, single connection (most common)
|
|
517
|
+
const revenue = await createRevenue({
|
|
518
|
+
connection,
|
|
519
|
+
bridges: {
|
|
520
|
+
source: {
|
|
521
|
+
async resolve(sourceId, sourceModel) {
|
|
522
|
+
const Model = mongoose.connection.models[sourceModel];
|
|
523
|
+
return Model ? await Model.findById(sourceId).lean() : null;
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
481
527
|
});
|
|
482
528
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
529
|
+
// Microservices (different DBs / HTTP)
|
|
530
|
+
bridges: {
|
|
531
|
+
source: {
|
|
532
|
+
async resolve(sourceId, sourceModel) {
|
|
533
|
+
if (sourceModel === 'Order') return await fetch(`http://orders-svc/${sourceId}`).then(r => r.json());
|
|
534
|
+
if (sourceModel === 'Invoice') return await invoiceDb.collection('invoices').findOne({ _id: sourceId });
|
|
535
|
+
return null;
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
}
|
|
490
539
|
|
|
491
|
-
//
|
|
492
|
-
|
|
540
|
+
// External systems (Stripe, Postgres)
|
|
541
|
+
bridges: {
|
|
542
|
+
source: {
|
|
543
|
+
async resolve(sourceId, sourceModel) {
|
|
544
|
+
if (sourceModel === 'StripeCharge') return await stripe.charges.retrieve(sourceId);
|
|
545
|
+
if (sourceModel === 'PostgresOrder') {
|
|
546
|
+
const { rows } = await pg.query('SELECT * FROM orders WHERE id = $1', [sourceId]);
|
|
547
|
+
return rows[0];
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
}
|
|
493
553
|
```
|
|
494
554
|
|
|
495
|
-
|
|
496
|
-
|
|
555
|
+
Use it in custom Arc routes for enrichment:
|
|
497
556
|
```typescript
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// Send notification
|
|
508
|
-
await sendPushNotification({
|
|
509
|
-
userId: result.transaction.customerId,
|
|
510
|
-
message: 'Payment verified!',
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
return result;
|
|
514
|
-
},
|
|
557
|
+
{
|
|
558
|
+
method: 'GET',
|
|
559
|
+
path: '/:id/with-source',
|
|
560
|
+
handler: async (req) => {
|
|
561
|
+
const txn = await revenue.repositories.transaction.getById(req.params.id);
|
|
562
|
+
const source = txn.sourceId
|
|
563
|
+
? await revenue.config.bridges?.source?.resolve?.(txn.sourceId, txn.sourceModel, req.scope)
|
|
564
|
+
: null;
|
|
565
|
+
return { ...txn, source };
|
|
515
566
|
},
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
revenue.withPlugin(notificationPlugin);
|
|
567
|
+
}
|
|
519
568
|
```
|
|
520
569
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
Built-in retry, circuit breaker, and idempotency:
|
|
524
|
-
|
|
570
|
+
For batch/list endpoints, use `resolveMany` to avoid N+1 queries:
|
|
525
571
|
```typescript
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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)
|
|
572
|
+
async resolveMany(refs, ctx) {
|
|
573
|
+
// Group by sourceModel, batch fetch, return Map<sourceId, doc>
|
|
574
|
+
}
|
|
542
575
|
```
|
|
543
576
|
|
|
544
|
-
###
|
|
545
|
-
|
|
546
|
-
No floating-point errors. All amounts in smallest currency unit (cents):
|
|
577
|
+
### LedgerBridge
|
|
547
578
|
|
|
548
579
|
```typescript
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
580
|
+
interface LedgerBridge {
|
|
581
|
+
onPaymentVerified?(transaction: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
|
|
582
|
+
onRefundProcessed?(original: Record<string, unknown>, refund: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
|
|
583
|
+
onSettlementCompleted?(settlement: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
|
|
584
|
+
}
|
|
585
|
+
```
|
|
562
586
|
|
|
563
|
-
|
|
564
|
-
const [a, b, c] = Money.usd(100).allocate([1, 1, 1]);
|
|
565
|
-
// [34, 33, 33] cents - total = 100 ✓
|
|
587
|
+
### TaxBridge
|
|
566
588
|
|
|
567
|
-
|
|
568
|
-
|
|
589
|
+
```typescript
|
|
590
|
+
interface TaxBridge {
|
|
591
|
+
computeTax?(amount: number, taxClass: string, ctx: RevenueContext): Promise<{ rate: number; amount: number; inclusive: boolean }>;
|
|
592
|
+
}
|
|
569
593
|
```
|
|
570
594
|
|
|
571
595
|
---
|
|
572
596
|
|
|
573
|
-
##
|
|
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
|
-
---
|
|
597
|
+
## Soft Delete & Force Cleanup
|
|
587
598
|
|
|
588
|
-
|
|
599
|
+
All financial repositories use mongokit's `softDeletePlugin` with `ttlDays: 365`. Calling `delete()` sets `deletedAt` instead of removing the document. After 365 days, MongoDB's TTL index automatically removes the document.
|
|
589
600
|
|
|
590
|
-
|
|
601
|
+
### Inherited methods (from softDeletePlugin)
|
|
591
602
|
|
|
592
603
|
```typescript
|
|
593
|
-
//
|
|
594
|
-
|
|
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
|
-
});
|
|
604
|
+
// Soft delete (sets deletedAt)
|
|
605
|
+
await revenue.repositories.transaction.delete(id);
|
|
607
606
|
|
|
608
|
-
//
|
|
609
|
-
await revenue.
|
|
607
|
+
// Restore a soft-deleted document
|
|
608
|
+
await revenue.repositories.transaction.restore(id);
|
|
610
609
|
|
|
611
|
-
//
|
|
612
|
-
await revenue.
|
|
610
|
+
// List soft-deleted documents
|
|
611
|
+
const trash = await revenue.repositories.transaction.getDeleted({ page: 1, limit: 50 });
|
|
613
612
|
|
|
614
|
-
//
|
|
615
|
-
await revenue.
|
|
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
|
-
]);
|
|
613
|
+
// Read a specific soft-deleted document
|
|
614
|
+
const doc = await revenue.repositories.transaction.getById(id, { includeDeleted: true });
|
|
628
615
|
```
|
|
629
616
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
## Submodule Imports
|
|
617
|
+
### Custom retention period
|
|
633
618
|
|
|
634
|
-
|
|
619
|
+
For compliance (US/EU financial records: ~7 years), override the plugin via `repositoryPlugins`:
|
|
635
620
|
|
|
636
621
|
```typescript
|
|
637
|
-
|
|
638
|
-
import { loggingPlugin, auditPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
|
|
622
|
+
import { softDeletePlugin } from '@classytic/mongokit';
|
|
639
623
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
624
|
+
const revenue = await createRevenue({
|
|
625
|
+
connection,
|
|
626
|
+
defaultCurrency: 'USD',
|
|
627
|
+
repositoryPlugins: {
|
|
628
|
+
transaction: [softDeletePlugin({ ttlDays: 2555 })], // 7 years
|
|
629
|
+
subscription: [softDeletePlugin({ ttlDays: 2555 })],
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
```
|
|
645
633
|
|
|
646
|
-
|
|
647
|
-
import { transactionSchema, subscriptionSchema } from '@classytic/revenue/schemas';
|
|
634
|
+
### Force-delete (admin / GDPR right-to-be-forgotten)
|
|
648
635
|
|
|
649
|
-
|
|
650
|
-
import { CreatePaymentSchema } from '@classytic/revenue/schemas/validation';
|
|
636
|
+
The repository's `Model` is the underlying Mongoose model — use it for raw operations when needed:
|
|
651
637
|
|
|
652
|
-
|
|
653
|
-
|
|
638
|
+
```typescript
|
|
639
|
+
// Custom Arc route for surgical force-delete
|
|
640
|
+
{
|
|
641
|
+
method: 'DELETE',
|
|
642
|
+
path: '/:id/force',
|
|
643
|
+
permissions: requireRoles('superadmin', 'compliance-officer'),
|
|
644
|
+
handler: async (req) => {
|
|
645
|
+
const id = req.params.id;
|
|
646
|
+
|
|
647
|
+
// Verify it IS soft-deleted first
|
|
648
|
+
const doc = await revenue.repositories.transaction.getById(id, {
|
|
649
|
+
includeDeleted: true,
|
|
650
|
+
throwOnNotFound: false,
|
|
651
|
+
});
|
|
652
|
+
if (!doc) return { error: 'Not found' };
|
|
653
|
+
if (!(doc as any).deletedAt) {
|
|
654
|
+
return { error: 'Document is not soft-deleted. Soft-delete first.' };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Hard delete via raw Mongoose model
|
|
658
|
+
await revenue.repositories.transaction.Model.deleteOne({ _id: id });
|
|
659
|
+
|
|
660
|
+
// Audit
|
|
661
|
+
await auditBridge.log({
|
|
662
|
+
action: 'force_delete',
|
|
663
|
+
resource: 'transaction',
|
|
664
|
+
resourceId: doc.publicId,
|
|
665
|
+
actor: req.user.id,
|
|
666
|
+
reason: req.body.reason,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return { success: true, publicId: doc.publicId };
|
|
670
|
+
},
|
|
671
|
+
}
|
|
672
|
+
```
|
|
654
673
|
|
|
655
|
-
|
|
656
|
-
import { reconcileSettlement } from '@classytic/revenue/reconciliation';
|
|
674
|
+
### Bulk force-cleanup (admin)
|
|
657
675
|
|
|
658
|
-
|
|
659
|
-
|
|
676
|
+
```typescript
|
|
677
|
+
{
|
|
678
|
+
method: 'POST',
|
|
679
|
+
path: '/force-cleanup',
|
|
680
|
+
permissions: requireRoles('superadmin'),
|
|
681
|
+
handler: async (req) => {
|
|
682
|
+
const { olderThanDays = 30, dryRun = true } = req.body;
|
|
683
|
+
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
|
684
|
+
const query = { deletedAt: { $ne: null, $lte: cutoff } };
|
|
685
|
+
|
|
686
|
+
if (dryRun) {
|
|
687
|
+
const count = await revenue.repositories.transaction.Model.countDocuments(query);
|
|
688
|
+
return { dryRun: true, wouldDelete: count };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const result = await revenue.repositories.transaction.Model.deleteMany(query);
|
|
692
|
+
return { deleted: result.deletedCount };
|
|
693
|
+
},
|
|
694
|
+
}
|
|
660
695
|
```
|
|
661
696
|
|
|
662
|
-
|
|
697
|
+
### Trash bin endpoint
|
|
663
698
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
699
|
+
```typescript
|
|
700
|
+
{
|
|
701
|
+
method: 'GET',
|
|
702
|
+
path: '/trash',
|
|
703
|
+
permissions: requireRoles('admin'),
|
|
704
|
+
handler: (req) => revenue.repositories.transaction.getDeleted({
|
|
705
|
+
page: req.query.page ?? 1,
|
|
706
|
+
limit: req.query.limit ?? 50,
|
|
707
|
+
sort: { deletedAt: -1 },
|
|
708
|
+
}),
|
|
709
|
+
}
|
|
710
|
+
```
|
|
667
711
|
|
|
668
|
-
|
|
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()` |
|
|
712
|
+
### Cleanup strategies
|
|
675
713
|
|
|
676
|
-
|
|
714
|
+
| Scenario | Approach |
|
|
715
|
+
|---|---|
|
|
716
|
+
| Default retention | TTL plugin handles it — no code needed |
|
|
717
|
+
| Compliance retention (7yr) | Override `softDeletePlugin({ ttlDays: 2555 })` |
|
|
718
|
+
| Test data cleanup | `Model.deleteMany({ deletedAt: { $ne: null } })` in test teardown |
|
|
719
|
+
| GDPR right-to-be-forgotten | Custom force-delete endpoint with audit log |
|
|
720
|
+
| Database size emergency | Bulk force-cleanup with dry-run support |
|
|
677
721
|
|
|
678
|
-
|
|
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
|
|
722
|
+
---
|
|
683
723
|
|
|
684
|
-
|
|
724
|
+
## Domain Verbs Reference
|
|
685
725
|
|
|
686
|
-
|
|
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 |
|
|
726
|
+
### TransactionRepository
|
|
693
727
|
|
|
694
|
-
|
|
728
|
+
| Method | Input | Returns | Description |
|
|
729
|
+
|---|---|---|---|
|
|
730
|
+
| `createPaymentIntent(params, ctx?)` | `{ amount, gateway, data?, metadata?, idempotencyKey? }` | `TransactionDocument` | Create transaction + call provider |
|
|
731
|
+
| `verify(intentId, options?, ctx?)` | `intentId, { verifiedBy? }` | `TransactionDocument` | Verify via provider, update status |
|
|
732
|
+
| `refund(txnId, amount?, options?, ctx?)` | `txnId, amount?, { reason? }` | `TransactionDocument` (refund) | Create refund transaction |
|
|
733
|
+
| `handleWebhook(provider, payload, headers?, ctx?)` | provider name + raw payload | `TransactionDocument \| null` | Process webhook, update transaction |
|
|
734
|
+
| `hold(txnId, options?, ctx?)` | `txnId, { amount?, reason?, holdUntil? }` | `TransactionDocument` | Place escrow hold |
|
|
735
|
+
| `release(txnId, options, ctx?)` | `txnId, { recipientId, recipientType, amount? }` | `TransactionDocument` | Release escrow |
|
|
736
|
+
| `split(txnId, rules, ctx?)` | `txnId, [{ type, recipientId, recipientType, rate }]` | `TransactionDocument` | Multi-party split |
|
|
695
737
|
|
|
696
|
-
|
|
738
|
+
### SubscriptionRepository
|
|
697
739
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
} from '@classytic/revenue';
|
|
705
|
-
|
|
706
|
-
try {
|
|
707
|
-
await revenue.monetization.create({ amount: -100 }); // Invalid
|
|
708
|
-
} catch (error) {
|
|
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);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
740
|
+
| Method | Input | Returns | Description |
|
|
741
|
+
|---|---|---|---|
|
|
742
|
+
| `activate(subId, options?, ctx?)` | `subId, { timestamp? }` | `SubscriptionDocument` | Activate, calculate period end |
|
|
743
|
+
| `cancel(subId, options?, ctx?)` | `subId, { immediate?, reason? }` | `SubscriptionDocument` | Cancel immediately or at period end |
|
|
744
|
+
| `pause(subId, options?, ctx?)` | `subId, { reason? }` | `SubscriptionDocument` | Pause subscription |
|
|
745
|
+
| `resume(subId, options?, ctx?)` | `subId, { extendPeriod? }` | `SubscriptionDocument` | Resume, optionally extend |
|
|
715
746
|
|
|
716
|
-
|
|
717
|
-
import { Result } from '@classytic/revenue';
|
|
747
|
+
### SettlementRepository
|
|
718
748
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
)
|
|
749
|
+
| Method | Input | Returns | Description |
|
|
750
|
+
|---|---|---|---|
|
|
751
|
+
| `schedule(params, ctx?)` | `{ organizationId, recipientId, amount, payoutMethod, ... }` | `SettlementDocument` | Schedule payout |
|
|
752
|
+
| `processPending(options?, ctx?)` | `{ limit?, organizationId?, dryRun? }` | `{ processed, succeeded, failed, settlements }` | Batch process pending |
|
|
753
|
+
| `complete(stlId, details?, ctx?)` | `stlId, { transferReference?, transactionHash? }` | `SettlementDocument` | Mark completed |
|
|
754
|
+
| `fail(stlId, reason, options?, ctx?)` | `stlId, reason, { retry?, code? }` | `SettlementDocument` | Mark failed or retry |
|
|
723
755
|
|
|
724
|
-
|
|
725
|
-
console.log(result.value);
|
|
726
|
-
} else {
|
|
727
|
-
console.error(result.error);
|
|
728
|
-
}
|
|
729
|
-
```
|
|
756
|
+
All inherited mongokit methods also available: `getAll`, `getById`, `getByQuery`, `getOne`, `create`, `update`, `delete`, `count`, `exists`, `distinct`, `aggregate`, `withTransaction`.
|
|
730
757
|
|
|
731
758
|
---
|
|
732
759
|
|
|
733
|
-
##
|
|
760
|
+
## Stripe-Style IDs
|
|
734
761
|
|
|
735
|
-
|
|
762
|
+
Via mongokit `customIdPlugin` + `prefixedId`:
|
|
736
763
|
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
CommissionInfo,
|
|
742
|
-
RevenueConfig,
|
|
743
|
-
} from '@classytic/revenue';
|
|
744
|
-
|
|
745
|
-
const transaction: TransactionDocument = await revenue.transactions.get(txId);
|
|
746
|
-
const commission: CommissionInfo = transaction.commission;
|
|
764
|
+
```
|
|
765
|
+
Transaction: txn_a7b3xk9m2p1q4d5e6f
|
|
766
|
+
Subscription: sub_x1y2z3a4b5c6d7e8f9g
|
|
767
|
+
Settlement: stl_m9n8o7p6q5r4s3t2u1v
|
|
747
768
|
```
|
|
748
769
|
|
|
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
|
|
770
|
+
Internal `_id` stays as MongoDB ObjectId. `publicId` is the external-facing identifier.
|
|
759
771
|
|
|
760
|
-
|
|
772
|
+
## Zod Schemas
|
|
761
773
|
|
|
762
|
-
|
|
774
|
+
Exported at `@classytic/revenue/schemas` for Arc OpenAPI auto-generation and runtime validation.
|
|
763
775
|
|
|
764
776
|
```typescript
|
|
765
777
|
import {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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({ ... }));
|
|
778
|
+
transactionCreateSchema, transactionUpdateSchema, transactionListFilterSchema,
|
|
779
|
+
subscriptionCreateSchema, subscriptionListFilterSchema,
|
|
780
|
+
settlementCreateSchema, settlementListFilterSchema,
|
|
781
|
+
paymentIntentSchema, paymentVerifySchema, refundSchema,
|
|
782
|
+
escrowHoldSchema, escrowReleaseSchema, splitRuleSchema,
|
|
783
|
+
} from '@classytic/revenue/schemas';
|
|
785
784
|
```
|
|
786
785
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
786
|
+
## Subpath Exports
|
|
787
|
+
|
|
788
|
+
| Import | Contents |
|
|
789
|
+
|---|---|
|
|
790
|
+
| `@classytic/revenue` | Main entry — engine, repos, types, everything |
|
|
791
|
+
| `@classytic/revenue/schemas` | Zod validators |
|
|
792
|
+
| `@classytic/revenue/enums` | Status/flow/type enums |
|
|
793
|
+
| `@classytic/revenue/events` | Event types, constants, transports |
|
|
794
|
+
| `@classytic/revenue/providers` | PaymentProvider base, response classes |
|
|
795
|
+
| `@classytic/revenue/bridges` | Bridge interfaces |
|
|
796
|
+
| `@classytic/revenue/utils` | Calculators (commission, tax, splits), Money class |
|
|
797
|
+
| `@classytic/revenue/core` | State machines, errors, Result type |
|
|
798
|
+
|
|
799
|
+
## Peer Dependencies
|
|
800
|
+
|
|
801
|
+
```json
|
|
802
|
+
{
|
|
803
|
+
"@classytic/mongokit": ">=3.5.6",
|
|
804
|
+
"mongoose": ">=9.0.0",
|
|
805
|
+
"zod": ">=4.0.0"
|
|
806
|
+
}
|
|
807
|
+
```
|
|
794
808
|
|
|
795
809
|
## License
|
|
796
810
|
|
|
797
|
-
MIT
|
|
798
|
-
|
|
799
|
-
---
|
|
800
|
-
|
|
801
|
-
## Support
|
|
802
|
-
|
|
803
|
-
- 📖 [Documentation](https://github.com/classytic/revenue#readme)
|
|
804
|
-
- 🐛 [Issues](https://github.com/classytic/revenue/issues)
|
|
805
|
-
- 💬 [Discussions](https://github.com/classytic/revenue/discussions)
|
|
811
|
+
MIT
|