@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +638 -632
  3. package/dist/audit-B39B0Sdq.mjs +53 -0
  4. package/dist/audit-DZ0eTr9g.d.mts +89 -0
  5. package/dist/bridges/index.d.mts +2 -0
  6. package/dist/bridges/index.mjs +1 -0
  7. package/dist/context-DRqSeTPM.d.mts +35 -0
  8. package/dist/core/state-machines.d.mts +35 -0
  9. package/dist/core/state-machines.mjs +134 -0
  10. package/dist/engine-types-CcjIb4Fy.d.mts +611 -0
  11. package/dist/enums/index.d.mts +3 -157
  12. package/dist/enums/index.mjs +3 -55
  13. package/dist/errors-DHa8JVQ-.mjs +92 -0
  14. package/dist/escrow.schema-BBv9oVEW.mjs +322 -0
  15. package/dist/escrow.schema-CC8XuD46.d.mts +629 -0
  16. package/dist/event-constants-CEMitnIV.mjs +53 -0
  17. package/dist/events/index.d.mts +3 -0
  18. package/dist/events/index.mjs +4 -0
  19. package/dist/index.d.mts +77 -9
  20. package/dist/index.mjs +465 -29
  21. package/dist/monetization.enums-BtiU3t8o.mjs +39 -0
  22. package/dist/monetization.enums-D2xbxXJM.d.mts +34 -0
  23. package/dist/plugins/plugin.interface.d.mts +28 -0
  24. package/dist/plugins/plugin.interface.mjs +26 -0
  25. package/dist/providers/index.d.mts +2 -3
  26. package/dist/providers/index.mjs +2 -2
  27. package/dist/{base-DCoyIUj6.mjs → registry-DhFMsSn5.mjs} +34 -36
  28. package/dist/{base-CsTlVQJe.d.mts → registry-SvIGPAx_.d.mts} +73 -66
  29. package/dist/repositories/create-repositories.d.mts +21 -0
  30. package/dist/repositories/create-repositories.mjs +12 -0
  31. package/dist/revenue-bridges-sdlrR85c.d.mts +145 -0
  32. package/dist/revenue-event-catalog-BX3g7RUi.d.mts +823 -0
  33. package/dist/revenue-event-catalog-LqxPnsU_.mjs +388 -0
  34. package/dist/settlement.repository-Cy3mMWGH.mjs +771 -0
  35. package/dist/shared/index.d.mts +2 -0
  36. package/dist/shared/index.mjs +4 -0
  37. package/dist/split.enums-CQE3ekH1.mjs +172 -0
  38. package/dist/split.enums-Dw4zCrcZ.d.mts +154 -0
  39. package/dist/splits-BAfY-a9P.mjs +123 -0
  40. package/dist/validators/index.d.mts +2 -0
  41. package/dist/validators/index.mjs +3 -0
  42. package/package.json +33 -37
  43. package/dist/application/services/index.d.mts +0 -4
  44. package/dist/application/services/index.mjs +0 -3
  45. package/dist/category-resolver-DV83N8ok.mjs +0 -284
  46. package/dist/commission-split-BzB8cd39.mjs +0 -485
  47. package/dist/core/events.d.mts +0 -294
  48. package/dist/core/events.mjs +0 -100
  49. package/dist/core/index.d.mts +0 -9
  50. package/dist/core/index.mjs +0 -8
  51. package/dist/errors-rRdOqnWx.d.mts +0 -787
  52. package/dist/escrow.enums-CZGrrdg7.mjs +0 -101
  53. package/dist/escrow.enums-DwdLuuve.d.mts +0 -78
  54. package/dist/idempotency-DaYcUGY1.mjs +0 -172
  55. package/dist/index-Dsp7H5Wb.d.mts +0 -471
  56. package/dist/infrastructure/plugins/index.d.mts +0 -239
  57. package/dist/infrastructure/plugins/index.mjs +0 -345
  58. package/dist/money-CvrDOijQ.mjs +0 -271
  59. package/dist/money-DPG8AtJ8.d.mts +0 -112
  60. package/dist/payment.enums-HAuAS9Pp.d.mts +0 -70
  61. package/dist/payment.enums-tEFVa-Xp.mjs +0 -69
  62. package/dist/plugin-BbK0OVHy.d.mts +0 -327
  63. package/dist/plugin-Cd_V04Em.mjs +0 -210
  64. package/dist/reconciliation/index.d.mts +0 -193
  65. package/dist/reconciliation/index.mjs +0 -192
  66. package/dist/retry-HHCOXYdn.d.mts +0 -186
  67. package/dist/revenue-BhdS7nXh.mjs +0 -553
  68. package/dist/schemas/index.d.mts +0 -2665
  69. package/dist/schemas/index.mjs +0 -717
  70. package/dist/schemas/validation.d.mts +0 -375
  71. package/dist/schemas/validation.mjs +0 -325
  72. package/dist/settlement.enums-DFhkqZEY.d.mts +0 -132
  73. package/dist/settlement.schema-DnNSFpGd.d.mts +0 -344
  74. package/dist/settlement.service-DjzAjezU.d.mts +0 -594
  75. package/dist/settlement.service-DmdKv0Zu.mjs +0 -2511
  76. package/dist/split.enums-BrjabxIX.mjs +0 -86
  77. package/dist/split.enums-DmskfLOM.d.mts +0 -43
  78. package/dist/tax-BoCt5cEd.d.mts +0 -61
  79. package/dist/tax-EQ15DO81.mjs +0 -162
  80. package/dist/transaction.enums-pCyMFT4Z.mjs +0 -96
  81. package/dist/utils/index.d.mts +0 -428
  82. 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
- > **Universal financial ledger for SaaS & marketplaces**
3
+ > Payment lifecycle engine transactions, subscriptions, escrow, settlements, commissions.
4
4
 
5
- Track subscriptions, purchases, refunds, escrow, and commission splits in **ONE Transaction model**. Built for enterprise with state machines, automatic retry logic, and multi-gateway support.
6
-
7
- [![npm version](https://badge.fury.io/js/@classytic%2Frevenue.svg)](https://www.npmjs.com/package/@classytic/revenue)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)](https://www.typescriptlang.org/)
9
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ## What Is This?
14
-
15
- A TypeScript library that handles **all financial transactions** in one unified model:
16
-
17
- ```typescript
18
- // Subscription payment
19
- { type: 'subscription', flow: 'inflow', amount: 2999 }
9
+ ## Install
20
10
 
21
- // Product purchase
22
- { type: 'product_order', flow: 'inflow', amount: 1500 }
23
-
24
- // Refund
25
- { type: 'refund', flow: 'outflow', amount: 1500 }
26
-
27
- // Operational expense
28
- { type: 'rent', flow: 'outflow', amount: 50000 }
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
- **One table. Query by type. Calculate P&L. Track cash flow.**
32
-
33
- ---
34
- ## Unified Cashflow Model (Shared Types)
35
-
36
- `@classytic/revenue` re-exports the unified transaction types from `@classytic/shared-types`. If you want a single Transaction model across revenue + payroll, define your schema using the shared types. The shared types are an interface only — you own the schema, enums, and indexes. There is no required “common schema”.
37
-
38
- Type safety is provided by `ITransaction` only. Transaction categories (`type`) are app-defined; `flow` (`inflow`/`outflow`) is the only shared enum.
16
+ ## Quick Start
39
17
 
40
18
  ```typescript
41
- import type { ITransaction } from '@classytic/shared-types';
42
- // or: import type { ITransaction } from '@classytic/revenue';
43
- ```
44
-
19
+ import { createRevenue } from '@classytic/revenue';
20
+ import { ManualProvider } from '@classytic/revenue-manual';
45
21
 
46
- ## Why Use This?
22
+ const revenue = await createRevenue({
23
+ connection: mongoose.connection,
24
+ defaultCurrency: 'BDT',
25
+ providers: { manual: new ManualProvider() },
26
+ });
47
27
 
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
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
- **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
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
- ## When to Use This
51
+ ## Architecture
66
52
 
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 |
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
- ## Installation
76
+ ## RevenueConfig
79
77
 
80
- ```bash
81
- npm install @classytic/revenue @classytic/shared-types mongoose zod
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
- **Peer Dependencies:**
85
- - `@classytic/shared-types` ^1.0.0
86
- - `mongoose` ^8.0.0 || ^9.0.0
87
- - `zod` ^4.0.0
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
- **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
- ```
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
- ## Quick Start
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
- ### 1. Define Your Transaction Model
128
+ multiTenant: true, // default: true
129
+ });
130
+ ```
100
131
 
101
- Copy the complete model from [examples/05-transaction-model.ts](./examples/05-transaction-model.ts):
132
+ ## RevenueEngine
102
133
 
103
134
  ```typescript
104
- import mongoose, { Schema } from 'mongoose';
105
- import type { ITransaction } from '@classytic/shared-types';
106
- import {
107
- TRANSACTION_FLOW_VALUES,
108
- TRANSACTION_STATUS_VALUES,
109
- gatewaySchema,
110
- commissionSchema,
111
- } from '@classytic/revenue';
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
- 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
145
-
146
- ```typescript
147
- import { Revenue } from '@classytic/revenue';
148
- import { ManualProvider } from '@classytic/revenue-manual';
145
+ ---
149
146
 
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
- ```
147
+ ## Arc Integration
159
148
 
160
- ### 3. Create a Payment
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
- // Create subscription payment
164
- const { transaction, subscription } = await revenue.monetization.create({
165
- data: {
166
- organizationId: 'org_123',
167
- customerId: 'user_456',
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
- console.log(transaction.status); // 'pending'
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
- ### 4. Verify Payment
179
-
180
- ```typescript
181
- await revenue.payments.verify(transaction._id);
182
-
183
- // Transaction: 'pending' → 'verified'
184
- // Subscription: 'pending' 'active'
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
- ### 5. Handle Refunds
188
-
221
+ **Frontend usage:**
189
222
  ```typescript
190
- // Full refund
191
- await revenue.payments.refund(transaction._id);
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
- // Partial refund: $10.00
194
- await revenue.payments.refund(transaction._id, 1000, {
195
- reason: 'customer_request',
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
- ## Core Concepts
242
+ ## Building a Custom Provider
202
243
 
203
- ### 1. Transaction Model (Required)
244
+ Every payment gateway implements the `PaymentProvider` abstract class. See `@classytic/revenue-manual` as the reference implementation.
204
245
 
205
- **The universal ledger.** Every financial event becomes a transaction:
246
+ ### PaymentProvider Interface
206
247
 
207
248
  ```typescript
208
- // Query subscriptions
209
- const subscriptions = await Transaction.find({
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
- ### 2. Payment Providers (Required)
252
+ export class StripeProvider extends PaymentProvider {
253
+ public override readonly name = 'stripe';
229
254
 
230
- **How money flows in.** Providers are swappable:
255
+ constructor(config: { apiKey: string }) {
256
+ super(config);
257
+ }
231
258
 
232
- ```typescript
233
- import { ManualProvider } from '@classytic/revenue-manual';
234
- // import { StripeProvider } from '@classytic/revenue-stripe'; // Coming soon
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
- revenue
237
- .withProvider('manual', new ManualProvider())
238
- .withProvider('stripe', new StripeProvider({ apiKey: '...' }));
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
- // Use any provider
241
- await revenue.monetization.create({
242
- gateway: 'manual', // or 'stripe'
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
- ### 3. Plugins (Optional)
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
- **Extend behavior.** Plugins add features without coupling:
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
- ```typescript
252
- import { loggingPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
253
-
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
- }));
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
- ```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',
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
- // Later: Renew
284
- await revenue.monetization.renew(subscription._id);
371
+ ### PaymentResult Status Map
285
372
 
286
- // Cancel
287
- await revenue.monetization.cancel(subscription._id, {
288
- reason: 'customer_requested',
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
- ### Create One-Time Purchase
380
+ ### Register Provider
293
381
 
294
382
  ```typescript
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
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
- ```typescript
312
- // By type (category)
313
- const subscriptions = await Transaction.find({
314
- type: 'platform_subscription',
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
- ## Advanced Features
402
+ ## Event System
334
403
 
335
- ### State Machines (Data Integrity)
404
+ Revenue uses `RevenueEventTransport` a structural superset of Arc's `DomainEvent`. Any Arc transport drops in with zero adapters.
336
405
 
337
- Prevent invalid transitions automatically:
406
+ ### RevenueDomainEvent
338
407
 
339
408
  ```typescript
340
- import { TRANSACTION_STATE_MACHINE } from '@classytic/revenue';
341
-
342
- // Valid
343
- await revenue.payments.verify(transaction._id); // pending → verified
344
-
345
- // ❌ Invalid (throws InvalidStateTransitionError)
346
- await revenue.payments.verify(completedTransaction._id); // completed verified
347
-
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
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
- **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
368
-
369
- ### Audit Trail (Track State Changes)
370
-
371
- Every state transition is automatically logged:
428
+ ### RevenueEventTransport
372
429
 
373
430
  ```typescript
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
- // ]
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
- ### Escrow (Marketplaces)
393
-
394
- Hold funds until conditions met:
438
+ ### Drop-in Transports
395
439
 
396
440
  ```typescript
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
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
- // Release to seller after delivery confirmed
408
- await revenue.escrow.release(transaction._id, {
409
- recipientId: 'seller_123',
410
- recipientType: 'organization',
411
- reason: 'delivery_confirmed',
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
- ```typescript
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%
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
- ### Events (React to Changes)
461
+ ### Pattern Matching
435
462
 
436
463
  ```typescript
437
- import { EventBus } from '@classytic/revenue/events';
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
- revenue.events.on('payment.verified', async (event) => {
440
- // Grant access
441
- await grantAccess(event.transaction.customerId);
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
- // Send email
444
- await sendEmail(event.transaction.customerId, 'Payment received!');
445
- });
493
+ ---
446
494
 
447
- revenue.events.on('subscription.cancelled', async (event) => {
448
- await removeAccess(event.subscription.customerId);
449
- });
495
+ ## Bridges
450
496
 
451
- // Other events:
452
- // - monetization.created, payment.failed, payment.refunded
453
- // - subscription.activated, subscription.renewed
454
- // - escrow.held, escrow.released, settlement.completed
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
- ### Tax Plugin (Optional)
511
+ ### SourceBridge Polymorphic Source Resolution
458
512
 
459
- Automatically calculate and track tax:
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
- import { createTaxPlugin } from '@classytic/revenue/plugins';
463
-
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
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
- console.log(transaction.tax);
484
- // {
485
- // rate: 0.15,
486
- // baseAmount: 10000,
487
- // taxAmount: 1500, // $15.00
488
- // totalAmount: 11500, // $115.00
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
- // Tax automatically reversed on refunds
492
- await revenue.payments.refund(transaction._id);
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
- ### Custom Plugins
496
-
555
+ Use it in custom Arc routes for enrichment:
497
556
  ```typescript
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
- },
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
- ### Resilience Patterns
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
- // 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)
572
+ async resolveMany(refs, ctx) {
573
+ // Group by sourceModel, batch fetch, return Map<sourceId, doc>
574
+ }
542
575
  ```
543
576
 
544
- ### Money Utilities
545
-
546
- No floating-point errors. All amounts in smallest currency unit (cents):
577
+ ### LedgerBridge
547
578
 
548
579
  ```typescript
549
- import { Money, toSmallestUnit, fromSmallestUnit } from '@classytic/revenue';
550
-
551
- // Create Money instances
552
- const price = Money.usd(1999); // $19.99
553
- const euro = Money.of(2999, 'EUR'); // €29.99
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
- // Fair allocation (handles rounding)
564
- const [a, b, c] = Money.usd(100).allocate([1, 1, 1]);
565
- // [34, 33, 33] cents - total = 100 ✓
587
+ ### TaxBridge
566
588
 
567
- // Formatting
568
- price.format(); // "$19.99"
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
- ## 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
- ---
597
+ ## Soft Delete & Force Cleanup
587
598
 
588
- ## Real-World Example
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
- **Course marketplace with affiliates:**
601
+ ### Inherited methods (from softDeletePlugin)
591
602
 
592
603
  ```typescript
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
- });
604
+ // Soft delete (sets deletedAt)
605
+ await revenue.repositories.transaction.delete(id);
607
606
 
608
- // 2. Payment verified → Grant course access
609
- await revenue.payments.verify(transaction._id);
607
+ // Restore a soft-deleted document
608
+ await revenue.repositories.transaction.restore(id);
610
609
 
611
- // 3. Hold in escrow (30-day refund window)
612
- await revenue.escrow.hold(transaction._id);
610
+ // List soft-deleted documents
611
+ const trash = await revenue.repositories.transaction.getDeleted({ page: 1, limit: 50 });
613
612
 
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
- ]);
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
- Tree-shakable imports for smaller bundles:
619
+ For compliance (US/EU financial records: ~7 years), override the plugin via `repositoryPlugins`:
635
620
 
636
621
  ```typescript
637
- // Plugins
638
- import { loggingPlugin, auditPlugin, createTaxPlugin } from '@classytic/revenue/plugins';
622
+ import { softDeletePlugin } from '@classytic/mongokit';
639
623
 
640
- // Enums
641
- import { TRANSACTION_STATUS, PAYMENT_STATUS } from '@classytic/revenue/enums';
642
-
643
- // Events
644
- import { EventBus } from '@classytic/revenue/events';
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
- // Schemas (Mongoose)
647
- import { transactionSchema, subscriptionSchema } from '@classytic/revenue/schemas';
634
+ ### Force-delete (admin / GDPR right-to-be-forgotten)
648
635
 
649
- // Validation (Zod)
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
- // Utilities
653
- import { retry, calculateCommission } from '@classytic/revenue/utils';
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
- // Reconciliation
656
- import { reconcileSettlement } from '@classytic/revenue/reconciliation';
674
+ ### Bulk force-cleanup (admin)
657
675
 
658
- // Services (advanced)
659
- import { MonetizationService } from '@classytic/revenue/services';
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
- ## API Reference
665
-
666
- ### Services
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
- | 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()` |
712
+ ### Cleanup strategies
675
713
 
676
- ### State Machines
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
- 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
722
+ ---
683
723
 
684
- ### Utilities
724
+ ## Domain Verbs Reference
685
725
 
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 |
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
- ## Error Handling
738
+ ### SubscriptionRepository
697
739
 
698
- ```typescript
699
- import {
700
- PaymentIntentCreationError,
701
- InvalidStateTransitionError,
702
- InvalidAmountError,
703
- RefundError,
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
- // Or use Result type (no exceptions)
717
- import { Result } from '@classytic/revenue';
747
+ ### SettlementRepository
718
748
 
719
- const result = await revenue.execute(
720
- () => revenue.payments.verify(txId),
721
- { idempotencyKey: 'verify_123' }
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
- if (result.ok) {
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
- ## TypeScript Support
760
+ ## Stripe-Style IDs
734
761
 
735
- Full type safety with auto-completion:
762
+ Via mongokit `customIdPlugin` + `prefixedId`:
736
763
 
737
- ```typescript
738
- import type {
739
- TransactionDocument,
740
- SubscriptionDocument,
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
- ## Built-in Plugins
774
+ Exported at `@classytic/revenue/schemas` for Arc OpenAPI auto-generation and runtime validation.
763
775
 
764
776
  ```typescript
765
777
  import {
766
- loggingPlugin,
767
- auditPlugin,
768
- metricsPlugin,
769
- createTaxPlugin
770
- } from '@classytic/revenue/plugins';
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
- ## Contributing
790
-
791
- Contributions welcome! Open an issue or submit a pull request on [GitHub](https://github.com/classytic/revenue).
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 © [Classytic](https://github.com/classytic)
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