@classytic/revenue 0.2.4 → 1.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/README.md +498 -501
- package/dist/actions-CwG-b7fR.d.ts +519 -0
- package/dist/core/index.d.ts +884 -0
- package/dist/core/index.js +2941 -0
- package/dist/core/index.js.map +1 -0
- package/dist/enums/index.d.ts +130 -0
- package/dist/enums/index.js +167 -0
- package/dist/enums/index.js.map +1 -0
- package/dist/index-BnJWVXuw.d.ts +378 -0
- package/dist/index-ChVD3P9k.d.ts +504 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +4353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +132 -0
- package/dist/providers/index.js +122 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/retry-80lBCmSe.d.ts +234 -0
- package/dist/schemas/index.d.ts +894 -0
- package/dist/schemas/index.js +524 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/validation.d.ts +309 -0
- package/dist/schemas/validation.js +249 -0
- package/dist/schemas/validation.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1632 -0
- package/dist/services/index.js.map +1 -0
- package/dist/split.enums-DHdM1YAV.d.ts +255 -0
- package/dist/split.schema-BPdFZMbU.d.ts +958 -0
- package/dist/utils/index.d.ts +24 -0
- package/dist/utils/index.js +1067 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +48 -32
- package/core/builder.js +0 -219
- package/core/container.js +0 -119
- package/core/errors.js +0 -262
- package/dist/types/core/builder.d.ts +0 -97
- package/dist/types/core/container.d.ts +0 -57
- package/dist/types/core/errors.d.ts +0 -122
- package/dist/types/enums/escrow.enums.d.ts +0 -24
- package/dist/types/enums/index.d.ts +0 -69
- package/dist/types/enums/monetization.enums.d.ts +0 -6
- package/dist/types/enums/payment.enums.d.ts +0 -16
- package/dist/types/enums/split.enums.d.ts +0 -25
- package/dist/types/enums/subscription.enums.d.ts +0 -15
- package/dist/types/enums/transaction.enums.d.ts +0 -24
- package/dist/types/index.d.ts +0 -22
- package/dist/types/providers/base.d.ts +0 -128
- package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
- package/dist/types/schemas/escrow/index.d.ts +0 -6
- package/dist/types/schemas/index.d.ts +0 -506
- package/dist/types/schemas/split/index.d.ts +0 -8
- package/dist/types/schemas/split/split.schema.d.ts +0 -142
- package/dist/types/schemas/subscription/index.d.ts +0 -152
- package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
- package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
- package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
- package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
- package/dist/types/schemas/transaction/index.d.ts +0 -202
- package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
- package/dist/types/services/escrow.service.d.ts +0 -51
- package/dist/types/services/monetization.service.d.ts +0 -193
- package/dist/types/services/payment.service.d.ts +0 -117
- package/dist/types/services/transaction.service.d.ts +0 -40
- package/dist/types/utils/category-resolver.d.ts +0 -46
- package/dist/types/utils/commission-split.d.ts +0 -56
- package/dist/types/utils/commission.d.ts +0 -29
- package/dist/types/utils/hooks.d.ts +0 -17
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/logger.d.ts +0 -12
- package/dist/types/utils/subscription/actions.d.ts +0 -28
- package/dist/types/utils/subscription/index.d.ts +0 -2
- package/dist/types/utils/subscription/period.d.ts +0 -47
- package/dist/types/utils/transaction-type.d.ts +0 -102
- package/enums/escrow.enums.js +0 -36
- package/enums/index.d.ts +0 -116
- package/enums/index.js +0 -110
- package/enums/monetization.enums.js +0 -15
- package/enums/payment.enums.js +0 -64
- package/enums/split.enums.js +0 -37
- package/enums/subscription.enums.js +0 -33
- package/enums/transaction.enums.js +0 -69
- package/index.js +0 -76
- package/providers/base.js +0 -162
- package/schemas/escrow/hold.schema.js +0 -62
- package/schemas/escrow/index.js +0 -15
- package/schemas/index.d.ts +0 -33
- package/schemas/index.js +0 -27
- package/schemas/split/index.js +0 -16
- package/schemas/split/split.schema.js +0 -86
- package/schemas/subscription/index.js +0 -17
- package/schemas/subscription/info.schema.js +0 -115
- package/schemas/subscription/plan.schema.js +0 -48
- package/schemas/transaction/common.schema.js +0 -22
- package/schemas/transaction/gateway.schema.js +0 -69
- package/schemas/transaction/index.js +0 -20
- package/schemas/transaction/payment.schema.js +0 -110
- package/services/escrow.service.js +0 -353
- package/services/monetization.service.js +0 -675
- package/services/payment.service.js +0 -535
- package/services/transaction.service.js +0 -142
- package/utils/category-resolver.js +0 -74
- package/utils/commission-split.js +0 -180
- package/utils/commission.js +0 -83
- package/utils/hooks.js +0 -44
- package/utils/index.d.ts +0 -164
- package/utils/index.js +0 -16
- package/utils/logger.js +0 -36
- package/utils/subscription/actions.js +0 -68
- package/utils/subscription/index.js +0 -20
- package/utils/subscription/period.js +0 -123
- package/utils/transaction-type.js +0 -254
package/README.md
CHANGED
|
@@ -1,689 +1,686 @@
|
|
|
1
1
|
# @classytic/revenue
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Modern, Type-safe Revenue Management for Node.js
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Subscriptions**: Create, renew, pause, cancel with lifecycle management
|
|
10
|
-
- **Payment Processing**: Multi-gateway support (Stripe, SSLCommerz, manual, etc.)
|
|
11
|
-
- **Transaction Management**: Income/expense tracking with verification and refunds
|
|
12
|
-
- **Escrow & Hold/Release**: Platform-as-intermediary payment flow (NEW in v0.1.0)
|
|
13
|
-
- **Multi-Party Splits**: Distribute revenue to platform, affiliates, partners (NEW)
|
|
14
|
-
- **Affiliate Commissions**: Built-in support for referral/affiliate programs (NEW)
|
|
15
|
-
- **Commission Tracking**: Automatic platform commission calculation with gateway fee deduction
|
|
16
|
-
- **Provider Pattern**: Pluggable payment providers (like LangChain/Vercel AI SDK)
|
|
17
|
-
- **Framework Agnostic**: Works with Express, Fastify, Next.js, or standalone
|
|
18
|
-
- **TypeScript Ready**: Full type definitions included
|
|
5
|
+
Enterprise-grade library for subscriptions, payments, escrow, and multi-party splits. Built with TypeScript, Zod validation, and resilience patterns.
|
|
19
6
|
|
|
20
7
|
## Installation
|
|
21
8
|
|
|
22
9
|
```bash
|
|
23
|
-
npm install @classytic/revenue
|
|
24
|
-
npm install @classytic/revenue-manual # For manual payments
|
|
10
|
+
npm install @classytic/revenue @classytic/revenue-manual
|
|
25
11
|
```
|
|
26
12
|
|
|
27
13
|
## Quick Start
|
|
28
14
|
|
|
29
|
-
###
|
|
15
|
+
### Fluent Builder API (Recommended)
|
|
30
16
|
|
|
31
|
-
```
|
|
32
|
-
import {
|
|
17
|
+
```typescript
|
|
18
|
+
import { Revenue, Money, loggingPlugin } from '@classytic/revenue';
|
|
33
19
|
import { ManualProvider } from '@classytic/revenue-manual';
|
|
34
20
|
|
|
21
|
+
const revenue = Revenue
|
|
22
|
+
.create({ defaultCurrency: 'USD' })
|
|
23
|
+
.withModels({ Transaction, Subscription })
|
|
24
|
+
.withProvider('manual', new ManualProvider())
|
|
25
|
+
.withProvider('stripe', new StripeProvider({ apiKey: '...' }))
|
|
26
|
+
.withPlugin(loggingPlugin())
|
|
27
|
+
.withRetry({ maxAttempts: 3, baseDelay: 1000 })
|
|
28
|
+
.withCircuitBreaker()
|
|
29
|
+
.withCommission(10, 2.5) // 10% platform, 2.5% gateway fee
|
|
30
|
+
.forEnvironment('production')
|
|
31
|
+
.build();
|
|
32
|
+
|
|
33
|
+
// Access services
|
|
34
|
+
await revenue.monetization.create({ ... });
|
|
35
|
+
await revenue.payments.verify(transactionId);
|
|
36
|
+
await revenue.escrow.hold(transactionId);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Shorthand Factory
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { createRevenue } from '@classytic/revenue';
|
|
43
|
+
|
|
35
44
|
const revenue = createRevenue({
|
|
36
|
-
models: { Transaction },
|
|
45
|
+
models: { Transaction, Subscription },
|
|
37
46
|
providers: { manual: new ManualProvider() },
|
|
47
|
+
options: { defaultCurrency: 'USD' },
|
|
38
48
|
});
|
|
49
|
+
```
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
const { transaction } = await revenue.monetization.create({
|
|
42
|
-
data: { customerId: user._id },
|
|
43
|
-
planKey: 'monthly',
|
|
44
|
-
monetizationType: 'subscription',
|
|
45
|
-
amount: 2999, // $29.99
|
|
46
|
-
gateway: 'manual',
|
|
47
|
-
paymentData: { method: 'card' },
|
|
48
|
-
});
|
|
51
|
+
---
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
await revenue.payments.verify(transaction._id);
|
|
52
|
-
```
|
|
53
|
+
## Core Concepts
|
|
53
54
|
|
|
54
|
-
###
|
|
55
|
+
### Money (Integer-Safe Currency)
|
|
55
56
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
const { transaction } = await revenue.monetization.create({
|
|
59
|
-
data: {
|
|
60
|
-
organizationId: vendor._id, // ← Multi-tenant
|
|
61
|
-
customerId: customer._id,
|
|
62
|
-
referenceId: order._id,
|
|
63
|
-
referenceModel: 'Order',
|
|
64
|
-
},
|
|
65
|
-
planKey: 'one_time',
|
|
66
|
-
monetizationType: 'purchase', // One-time purchase
|
|
67
|
-
amount: 1500,
|
|
68
|
-
gateway: 'manual',
|
|
69
|
-
paymentData: { method: 'bkash' },
|
|
70
|
-
});
|
|
71
|
-
```
|
|
57
|
+
```typescript
|
|
58
|
+
import { Money } from '@classytic/revenue';
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
// Create from cents (safe)
|
|
61
|
+
const price = Money.usd(1999); // $19.99
|
|
62
|
+
const price2 = Money.of(19.99, 'USD'); // Auto-converts to 1999 cents
|
|
74
63
|
|
|
75
|
-
|
|
64
|
+
// Arithmetic
|
|
65
|
+
const discounted = price.multiply(0.9); // 10% off
|
|
66
|
+
const withTax = price.add(Money.usd(200));
|
|
67
|
+
const perPerson = price.divide(3);
|
|
76
68
|
|
|
77
|
-
|
|
69
|
+
// Format
|
|
70
|
+
console.log(price.format()); // "$19.99"
|
|
71
|
+
console.log(price.toUnit()); // 19.99
|
|
72
|
+
console.log(price.amount); // 1999 (integer cents)
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
TRANSACTION_TYPE_VALUES,
|
|
83
|
-
TRANSACTION_STATUS_VALUES,
|
|
84
|
-
} from '@classytic/revenue/enums';
|
|
85
|
-
import {
|
|
86
|
-
gatewaySchema,
|
|
87
|
-
paymentDetailsSchema,
|
|
88
|
-
} from '@classytic/revenue/schemas';
|
|
74
|
+
// Split fairly (handles rounding)
|
|
75
|
+
const [a, b, c] = Money.usd(100).allocate([1, 1, 1]); // [34, 33, 33] cents
|
|
76
|
+
```
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
// ============ REQUIRED BY LIBRARY ============
|
|
92
|
-
amount: { type: Number, required: true, min: 0 },
|
|
93
|
-
type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true }, // 'income' | 'expense'
|
|
94
|
-
method: { type: String, required: true }, // 'manual' | 'bkash' | 'card' | etc.
|
|
95
|
-
status: { type: String, enum: TRANSACTION_STATUS_VALUES, required: true },
|
|
96
|
-
category: { type: String, required: true }, // Your custom categories
|
|
97
|
-
|
|
98
|
-
// ============ MULTI-TENANT (optional) ============
|
|
99
|
-
organizationId: { type: String, index: true }, // For multi-tenant platforms
|
|
100
|
-
|
|
101
|
-
// ============ LIBRARY SCHEMAS (nested) ============
|
|
102
|
-
gateway: gatewaySchema, // Payment gateway details
|
|
103
|
-
paymentDetails: paymentDetailsSchema, // Payment info (wallet, bank, etc.)
|
|
104
|
-
|
|
105
|
-
// ============ POLYMORPHIC REFERENCE (recommended) ============
|
|
106
|
-
// Links transaction to any entity (Order, Subscription, Enrollment, etc.)
|
|
107
|
-
referenceId: {
|
|
108
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
109
|
-
refPath: 'referenceModel',
|
|
110
|
-
},
|
|
111
|
-
referenceModel: {
|
|
112
|
-
type: String,
|
|
113
|
-
enum: ['Subscription', 'Order', 'Enrollment', 'Membership'], // Your models
|
|
114
|
-
},
|
|
78
|
+
### Result Type (No Throws)
|
|
115
79
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
import { Result, ok, err, match } from '@classytic/revenue';
|
|
82
|
+
|
|
83
|
+
// Execute with Result
|
|
84
|
+
const result = await revenue.execute(
|
|
85
|
+
() => riskyOperation(),
|
|
86
|
+
{ idempotencyKey: 'order_123' }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Pattern matching
|
|
90
|
+
match(result, {
|
|
91
|
+
ok: (value) => console.log('Success:', value),
|
|
92
|
+
err: (error) => console.log('Error:', error.message),
|
|
93
|
+
});
|
|
125
94
|
|
|
126
|
-
|
|
95
|
+
// Or simple check
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
console.log(result.value);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(result.error);
|
|
100
|
+
}
|
|
127
101
|
```
|
|
128
102
|
|
|
129
|
-
|
|
103
|
+
### Type-Safe Events
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| `subscriptionInfoSchema` | Subscription details (for Order models) | `planKey`, `startDate`, `endDate` |
|
|
105
|
+
```typescript
|
|
106
|
+
// Subscribe to events
|
|
107
|
+
revenue.on('payment.succeeded', (event) => {
|
|
108
|
+
console.log('Transaction:', event.transactionId);
|
|
109
|
+
console.log('Amount:', event.transaction.amount);
|
|
110
|
+
});
|
|
138
111
|
|
|
139
|
-
|
|
112
|
+
revenue.on('subscription.renewed', (event) => {
|
|
113
|
+
sendEmail(event.subscription.customerId, 'Renewed!');
|
|
114
|
+
});
|
|
140
115
|
|
|
141
|
-
|
|
142
|
-
|
|
116
|
+
revenue.on('escrow.released', (event) => {
|
|
117
|
+
console.log('Released:', event.releasedAmount);
|
|
118
|
+
});
|
|
143
119
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
120
|
+
// Wildcard - catch all events
|
|
121
|
+
revenue.on('*', (event) => {
|
|
122
|
+
analytics.track(event.type, event);
|
|
147
123
|
});
|
|
148
124
|
```
|
|
149
125
|
|
|
150
|
-
|
|
126
|
+
### Validation (Zod v4)
|
|
151
127
|
|
|
152
|
-
|
|
128
|
+
```typescript
|
|
129
|
+
import { CreatePaymentSchema, validate, safeValidate } from '@classytic/revenue';
|
|
153
130
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const { subscription, transaction, paymentIntent } =
|
|
157
|
-
await revenue.monetization.create({
|
|
158
|
-
data: { organizationId, customerId },
|
|
159
|
-
planKey: 'monthly',
|
|
160
|
-
amount: 1500,
|
|
161
|
-
currency: 'BDT',
|
|
162
|
-
gateway: 'manual',
|
|
163
|
-
paymentData: { method: 'bkash', walletNumber: '01712345678' },
|
|
164
|
-
});
|
|
131
|
+
// Validate input (throws on error)
|
|
132
|
+
const payment = validate(CreatePaymentSchema, userInput);
|
|
165
133
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
134
|
+
// Safe validation (returns result)
|
|
135
|
+
const result = safeValidate(CreatePaymentSchema, userInput);
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
console.log(result.error.issues);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Services
|
|
144
|
+
|
|
145
|
+
### Monetization (Purchases & Subscriptions)
|
|
169
146
|
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
```typescript
|
|
148
|
+
// One-time purchase
|
|
149
|
+
const { transaction, paymentIntent } = await revenue.monetization.create({
|
|
150
|
+
data: {
|
|
151
|
+
customerId: user._id,
|
|
152
|
+
organizationId: org._id,
|
|
153
|
+
referenceId: order._id,
|
|
154
|
+
referenceModel: 'Order',
|
|
155
|
+
},
|
|
156
|
+
planKey: 'one_time',
|
|
157
|
+
monetizationType: 'purchase',
|
|
158
|
+
amount: 1500,
|
|
172
159
|
gateway: 'manual',
|
|
173
|
-
paymentData: { method: '
|
|
160
|
+
paymentData: { method: 'card' },
|
|
174
161
|
});
|
|
175
162
|
|
|
176
|
-
//
|
|
177
|
-
await revenue.monetization.
|
|
178
|
-
|
|
163
|
+
// Recurring subscription
|
|
164
|
+
const { subscription, transaction } = await revenue.monetization.create({
|
|
165
|
+
data: { customerId: user._id },
|
|
166
|
+
planKey: 'monthly',
|
|
167
|
+
monetizationType: 'subscription',
|
|
168
|
+
amount: 2999,
|
|
169
|
+
gateway: 'stripe',
|
|
170
|
+
});
|
|
179
171
|
|
|
180
|
-
//
|
|
172
|
+
// Lifecycle management
|
|
173
|
+
await revenue.monetization.activate(subscription._id);
|
|
174
|
+
await revenue.monetization.renew(subscription._id, { gateway: 'stripe' });
|
|
175
|
+
await revenue.monetization.pause(subscription._id, { reason: 'Vacation' });
|
|
176
|
+
await revenue.monetization.resume(subscription._id);
|
|
181
177
|
await revenue.monetization.cancel(subscription._id, { immediate: true });
|
|
182
178
|
```
|
|
183
179
|
|
|
184
180
|
### Payments
|
|
185
181
|
|
|
186
|
-
```
|
|
187
|
-
// Verify payment
|
|
188
|
-
const { transaction } = await revenue.payments.verify(
|
|
189
|
-
|
|
190
|
-
}
|
|
182
|
+
```typescript
|
|
183
|
+
// Verify payment
|
|
184
|
+
const { transaction, paymentResult } = await revenue.payments.verify(
|
|
185
|
+
transactionId,
|
|
186
|
+
{ verifiedBy: adminId }
|
|
187
|
+
);
|
|
191
188
|
|
|
192
|
-
// Get
|
|
193
|
-
const { status } = await revenue.payments.getStatus(
|
|
189
|
+
// Get status
|
|
190
|
+
const { status, provider } = await revenue.payments.getStatus(transactionId);
|
|
194
191
|
|
|
195
|
-
//
|
|
196
|
-
const {
|
|
192
|
+
// Full refund
|
|
193
|
+
const { refundTransaction } = await revenue.payments.refund(transactionId);
|
|
194
|
+
|
|
195
|
+
// Partial refund
|
|
196
|
+
const { refundTransaction } = await revenue.payments.refund(
|
|
197
197
|
transactionId,
|
|
198
|
-
500,
|
|
199
|
-
{ reason: '
|
|
198
|
+
500, // Amount in cents
|
|
199
|
+
{ reason: 'Partial return' }
|
|
200
200
|
);
|
|
201
201
|
|
|
202
|
-
// Handle webhook
|
|
202
|
+
// Handle webhook
|
|
203
203
|
const { event, transaction } = await revenue.payments.handleWebhook(
|
|
204
204
|
'stripe',
|
|
205
|
-
|
|
205
|
+
payload,
|
|
206
206
|
headers
|
|
207
207
|
);
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
###
|
|
210
|
+
### Escrow (Hold/Release)
|
|
211
211
|
|
|
212
|
-
```
|
|
213
|
-
//
|
|
214
|
-
|
|
212
|
+
```typescript
|
|
213
|
+
// Hold funds in escrow
|
|
214
|
+
await revenue.escrow.hold(transactionId, {
|
|
215
|
+
holdUntil: new Date('2024-12-31'),
|
|
216
|
+
reason: 'Awaiting delivery confirmation',
|
|
217
|
+
});
|
|
215
218
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
// Release to recipient
|
|
220
|
+
await revenue.escrow.release(transactionId, {
|
|
221
|
+
recipientId: vendorId,
|
|
222
|
+
recipientType: 'organization',
|
|
223
|
+
amount: 800, // Partial release
|
|
224
|
+
});
|
|
221
225
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// Multi-party split
|
|
227
|
+
await revenue.escrow.split(transactionId, [
|
|
228
|
+
{ type: 'platform_commission', recipientId: 'platform', rate: 0.10 },
|
|
229
|
+
{ type: 'affiliate_commission', recipientId: 'aff_123', rate: 0.05 },
|
|
230
|
+
]);
|
|
227
231
|
|
|
228
|
-
|
|
232
|
+
// Cancel hold
|
|
233
|
+
await revenue.escrow.cancelHold(transactionId, { reason: 'Order cancelled' });
|
|
234
|
+
```
|
|
229
235
|
|
|
230
|
-
|
|
236
|
+
---
|
|
231
237
|
|
|
232
|
-
|
|
233
|
-
- **EXPENSE** (`'expense'`): Money going out - refunds, payouts
|
|
238
|
+
## Plugins
|
|
234
239
|
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
```typescript
|
|
241
|
+
import { loggingPlugin, auditPlugin, metricsPlugin, definePlugin } from '@classytic/revenue';
|
|
242
|
+
|
|
243
|
+
// Built-in plugins
|
|
244
|
+
const revenue = Revenue
|
|
245
|
+
.create()
|
|
246
|
+
.withPlugin(loggingPlugin({ level: 'info' }))
|
|
247
|
+
.withPlugin(auditPlugin({ store: saveToDatabase }))
|
|
248
|
+
.withPlugin(metricsPlugin({ onMetric: sendToDatadog }))
|
|
249
|
+
.build();
|
|
250
|
+
|
|
251
|
+
// Custom plugin
|
|
252
|
+
const rateLimitPlugin = definePlugin({
|
|
253
|
+
name: 'rate-limit',
|
|
254
|
+
hooks: {
|
|
255
|
+
'payment.create.before': async (ctx, input, next) => {
|
|
256
|
+
if (await isRateLimited(input.customerId)) {
|
|
257
|
+
throw new Error('Rate limited');
|
|
258
|
+
}
|
|
259
|
+
return next();
|
|
243
260
|
},
|
|
244
261
|
},
|
|
245
262
|
});
|
|
246
263
|
```
|
|
247
264
|
|
|
248
|
-
|
|
249
|
-
- Refund creates NEW transaction with `type: 'expense'`
|
|
250
|
-
- Original transaction status becomes `'refunded'` or `'partially_refunded'`
|
|
251
|
-
- Both linked via metadata for audit trail
|
|
252
|
-
- Calculate net: `SUM(income) - SUM(expense)`
|
|
265
|
+
---
|
|
253
266
|
|
|
254
|
-
##
|
|
267
|
+
## Resilience
|
|
255
268
|
|
|
256
|
-
|
|
269
|
+
### Retry with Exponential Backoff
|
|
257
270
|
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
models: { Transaction },
|
|
261
|
-
config: {
|
|
262
|
-
categoryMappings: {
|
|
263
|
-
Order: 'order_subscription',
|
|
264
|
-
PlatformSubscription: 'platform_subscription',
|
|
265
|
-
Membership: 'gym_membership',
|
|
266
|
-
Enrollment: 'course_enrollment',
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
271
|
+
```typescript
|
|
272
|
+
import { retry, retryWithResult, isRetryableError } from '@classytic/revenue';
|
|
270
273
|
|
|
271
|
-
//
|
|
272
|
-
await
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Simple retry
|
|
275
|
+
const data = await retry(
|
|
276
|
+
() => fetchPaymentStatus(id),
|
|
277
|
+
{
|
|
278
|
+
maxAttempts: 5,
|
|
279
|
+
baseDelay: 1000,
|
|
280
|
+
maxDelay: 30000,
|
|
281
|
+
backoffMultiplier: 2,
|
|
282
|
+
jitter: 0.1,
|
|
283
|
+
}
|
|
284
|
+
);
|
|
278
285
|
|
|
279
|
-
|
|
286
|
+
// Retry with Result (no throws)
|
|
287
|
+
const result = await retryWithResult(() => processPayment());
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
console.log('All retries failed:', result.error.errors);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
280
292
|
|
|
281
|
-
|
|
293
|
+
### Circuit Breaker
|
|
282
294
|
|
|
283
|
-
|
|
295
|
+
```typescript
|
|
296
|
+
import { CircuitBreaker, createCircuitBreaker } from '@classytic/revenue';
|
|
284
297
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
config: {
|
|
289
|
-
// Commission rates by category
|
|
290
|
-
commissionRates: {
|
|
291
|
-
'product_order': 0.10, // 10% platform commission
|
|
292
|
-
'course_enrollment': 0.10, // 10% on courses
|
|
293
|
-
'gym_membership': 0, // No commission
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
// Gateway fees (deducted from commission)
|
|
297
|
-
gatewayFeeRates: {
|
|
298
|
-
'stripe': 0.029, // 2.9% Stripe fee
|
|
299
|
-
'bkash': 0.018, // 1.8% bKash fee
|
|
300
|
-
'manual': 0, // No fee
|
|
301
|
-
},
|
|
302
|
-
},
|
|
298
|
+
const breaker = createCircuitBreaker({
|
|
299
|
+
failureThreshold: 5,
|
|
300
|
+
resetTimeout: 30000,
|
|
303
301
|
});
|
|
304
302
|
|
|
305
|
-
|
|
306
|
-
const { transaction } = await revenue.monetization.create({
|
|
307
|
-
amount: 10000, // $100
|
|
308
|
-
entity: 'ProductOrder', // → 10% commission
|
|
309
|
-
gateway: 'stripe', // → 2.9% fee
|
|
310
|
-
});
|
|
303
|
+
const result = await breaker.execute(() => callExternalAPI());
|
|
311
304
|
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
// rate: 0.10,
|
|
315
|
-
// grossAmount: 1000, // $10 (10% of $100)
|
|
316
|
-
// gatewayFeeAmount: 290, // $2.90 (2.9% of $100)
|
|
317
|
-
// netAmount: 710, // $7.10 (platform keeps)
|
|
318
|
-
// status: 'pending'
|
|
319
|
-
// }
|
|
320
|
-
|
|
321
|
-
// Query pending commissions
|
|
322
|
-
const pending = await Transaction.find({ 'commission.status': 'pending' });
|
|
305
|
+
// Check state
|
|
306
|
+
console.log(breaker.getState()); // 'closed' | 'open' | 'half-open'
|
|
323
307
|
```
|
|
324
308
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
**See:** [`examples/commission-tracking.js`](examples/commission-tracking.js) for complete guide.
|
|
309
|
+
### Idempotency
|
|
328
310
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Universal helpers for period calculation, proration, and action eligibility:
|
|
311
|
+
```typescript
|
|
312
|
+
import { IdempotencyManager } from '@classytic/revenue';
|
|
332
313
|
|
|
333
|
-
|
|
334
|
-
import {
|
|
335
|
-
// Period calculation
|
|
336
|
-
calculatePeriodRange,
|
|
337
|
-
calculateProratedAmount,
|
|
338
|
-
addDuration,
|
|
339
|
-
|
|
340
|
-
// Action eligibility
|
|
341
|
-
canRenewSubscription,
|
|
342
|
-
canPauseSubscription,
|
|
343
|
-
isSubscriptionActive,
|
|
344
|
-
} from '@classytic/revenue/utils';
|
|
345
|
-
|
|
346
|
-
// Calculate period
|
|
347
|
-
const { startDate, endDate } = calculatePeriodRange({
|
|
348
|
-
duration: 30,
|
|
349
|
-
unit: 'days',
|
|
350
|
-
});
|
|
314
|
+
const idempotency = new IdempotencyManager({ ttl: 86400000 }); // 24h
|
|
351
315
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
asOfDate: new Date(),
|
|
358
|
-
});
|
|
316
|
+
const result = await idempotency.execute(
|
|
317
|
+
'payment_order_123',
|
|
318
|
+
{ amount: 1999, customerId: 'cust_1' },
|
|
319
|
+
() => chargeCard()
|
|
320
|
+
);
|
|
359
321
|
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
await revenue.monetization.renew(membership.subscriptionId);
|
|
363
|
-
}
|
|
322
|
+
// Same key + same params = cached result
|
|
323
|
+
// Same key + different params = error
|
|
364
324
|
```
|
|
365
325
|
|
|
366
|
-
|
|
326
|
+
---
|
|
367
327
|
|
|
368
|
-
|
|
328
|
+
## Transaction Model Setup
|
|
369
329
|
|
|
370
|
-
|
|
330
|
+
**ONE Transaction model = Universal Financial Ledger**
|
|
371
331
|
|
|
372
|
-
|
|
373
|
-
// 1. Customer makes purchase
|
|
374
|
-
const { transaction } = await revenue.monetization.create({
|
|
375
|
-
amount: 1000,
|
|
376
|
-
gateway: 'stripe',
|
|
377
|
-
// ...
|
|
378
|
-
});
|
|
332
|
+
The Transaction model is the ONLY required model. Use it for subscriptions, purchases, refunds, and operational expenses. The Subscription model is **optional** (only for tracking subscription state).
|
|
379
333
|
|
|
380
|
-
|
|
381
|
-
|
|
334
|
+
```typescript
|
|
335
|
+
import mongoose from 'mongoose';
|
|
336
|
+
import {
|
|
337
|
+
// Enums
|
|
338
|
+
TRANSACTION_TYPE_VALUES,
|
|
339
|
+
TRANSACTION_STATUS_VALUES,
|
|
340
|
+
// Mongoose schemas (compose into your model)
|
|
341
|
+
gatewaySchema,
|
|
342
|
+
paymentDetailsSchema,
|
|
343
|
+
commissionSchema,
|
|
344
|
+
holdSchema,
|
|
345
|
+
splitSchema,
|
|
346
|
+
} from '@classytic/revenue';
|
|
382
347
|
|
|
383
|
-
//
|
|
384
|
-
|
|
348
|
+
// Your app-specific categories
|
|
349
|
+
const CATEGORIES = [
|
|
350
|
+
'platform_subscription',
|
|
351
|
+
'course_enrollment',
|
|
352
|
+
'product_order',
|
|
353
|
+
'refund',
|
|
354
|
+
'rent',
|
|
355
|
+
'salary',
|
|
356
|
+
'utilities',
|
|
357
|
+
];
|
|
385
358
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{ type:
|
|
389
|
-
{ type:
|
|
390
|
-
|
|
391
|
-
|
|
359
|
+
const transactionSchema = new mongoose.Schema({
|
|
360
|
+
// Core fields
|
|
361
|
+
organizationId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
|
|
362
|
+
customerId: { type: mongoose.Schema.Types.ObjectId, index: true },
|
|
363
|
+
type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true }, // income | expense
|
|
364
|
+
category: { type: String, enum: CATEGORIES, index: true },
|
|
365
|
+
status: { type: String, enum: TRANSACTION_STATUS_VALUES, default: 'pending' },
|
|
366
|
+
amount: { type: Number, required: true, min: 0 },
|
|
367
|
+
currency: { type: String, default: 'USD' },
|
|
368
|
+
method: { type: String, required: true },
|
|
392
369
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
370
|
+
// Library schemas (compose, don't spread)
|
|
371
|
+
gateway: gatewaySchema,
|
|
372
|
+
commission: commissionSchema,
|
|
373
|
+
paymentDetails: paymentDetailsSchema,
|
|
374
|
+
hold: holdSchema,
|
|
375
|
+
splits: [splitSchema],
|
|
399
376
|
|
|
400
|
-
|
|
377
|
+
// Polymorphic reference (link to any entity)
|
|
378
|
+
referenceId: { type: mongoose.Schema.Types.ObjectId, refPath: 'referenceModel' },
|
|
379
|
+
referenceModel: { type: String, enum: ['Subscription', 'Order', 'Enrollment'] },
|
|
401
380
|
|
|
402
|
-
|
|
403
|
-
|
|
381
|
+
// Idempotency & verification
|
|
382
|
+
idempotencyKey: { type: String, unique: true, sparse: true },
|
|
383
|
+
verifiedAt: Date,
|
|
384
|
+
verifiedBy: mongoose.Schema.Types.Mixed, // ObjectId or 'system'
|
|
385
|
+
|
|
386
|
+
// Refunds
|
|
387
|
+
refundedAmount: Number,
|
|
388
|
+
refundedAt: Date,
|
|
404
389
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
0.10, // platform rate
|
|
408
|
-
0.029, // gateway fee
|
|
409
|
-
{
|
|
410
|
-
affiliateRate: 0.05,
|
|
411
|
-
affiliateId: 'affiliate-123',
|
|
412
|
-
}
|
|
413
|
-
);
|
|
390
|
+
metadata: mongoose.Schema.Types.Mixed,
|
|
391
|
+
}, { timestamps: true });
|
|
414
392
|
|
|
415
|
-
|
|
416
|
-
// {
|
|
417
|
-
// grossAmount: 500, // Platform: 10%
|
|
418
|
-
// netAmount: 355, // After gateway fee (2.9%)
|
|
419
|
-
// affiliate: {
|
|
420
|
-
// grossAmount: 250, // Affiliate: 5%
|
|
421
|
-
// netAmount: 250,
|
|
422
|
-
// },
|
|
423
|
-
// splits: [...]
|
|
424
|
-
// }
|
|
393
|
+
export const Transaction = mongoose.model('Transaction', transactionSchema);
|
|
425
394
|
```
|
|
426
395
|
|
|
427
|
-
###
|
|
396
|
+
### Available Schemas
|
|
428
397
|
|
|
429
|
-
|
|
430
|
-
|
|
398
|
+
| Schema | Purpose | Usage |
|
|
399
|
+
|--------|---------|-------|
|
|
400
|
+
| `gatewaySchema` | Payment gateway details | `gateway: gatewaySchema` |
|
|
401
|
+
| `commissionSchema` | Platform commission | `commission: commissionSchema` |
|
|
402
|
+
| `paymentDetailsSchema` | Manual payment info | `paymentDetails: paymentDetailsSchema` |
|
|
403
|
+
| `holdSchema` | Escrow hold/release | `hold: holdSchema` |
|
|
404
|
+
| `splitSchema` | Multi-party splits | `splits: [splitSchema]` |
|
|
405
|
+
| `currentPaymentSchema` | For Order/Subscription models | `currentPayment: currentPaymentSchema` |
|
|
431
406
|
|
|
432
|
-
|
|
433
|
-
{ type: 'platform_commission', recipientId: 'platform', rate: 0.10 },
|
|
434
|
-
{ type: 'affiliate_commission', recipientId: 'level1', rate: 0.05 },
|
|
435
|
-
{ type: 'affiliate_commission', recipientId: 'level2', rate: 0.02 },
|
|
436
|
-
{ type: 'partner_commission', recipientId: 'partner', rate: 0.03 },
|
|
437
|
-
], 0.029); // Gateway fee
|
|
438
|
-
|
|
439
|
-
// Returns splits array with calculated amounts
|
|
440
|
-
// Organization receives: 8000 (80%)
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Schemas for Escrow
|
|
444
|
-
|
|
445
|
-
Add to your transaction model when using escrow:
|
|
407
|
+
**Usage:** Import and use as nested objects (NOT spread):
|
|
446
408
|
|
|
447
|
-
```
|
|
448
|
-
import {
|
|
409
|
+
```typescript
|
|
410
|
+
import { gatewaySchema, commissionSchema } from '@classytic/revenue';
|
|
449
411
|
|
|
450
|
-
|
|
451
|
-
|
|
412
|
+
const schema = new mongoose.Schema({
|
|
413
|
+
gateway: gatewaySchema, // ✅ Correct - nested
|
|
414
|
+
commission: commissionSchema,
|
|
415
|
+
// ...gatewaySchema, // ❌ Wrong - don't spread
|
|
416
|
+
});
|
|
452
417
|
```
|
|
453
418
|
|
|
454
|
-
|
|
455
|
-
- E-commerce marketplaces (hold until delivery confirmed)
|
|
456
|
-
- Course platforms with affiliates
|
|
457
|
-
- Group buy / crowdfunding
|
|
458
|
-
- Multi-level marketing
|
|
459
|
-
- SaaS reseller programs
|
|
419
|
+
---
|
|
460
420
|
|
|
461
|
-
|
|
421
|
+
## Group Payments (Split Pay)
|
|
462
422
|
|
|
463
|
-
|
|
423
|
+
Multiple payers can contribute to one purchase using `referenceId`:
|
|
464
424
|
|
|
465
|
-
|
|
425
|
+
```typescript
|
|
426
|
+
// Order total: $100 (10000 cents)
|
|
427
|
+
const orderId = new mongoose.Types.ObjectId();
|
|
428
|
+
const orderTotal = 10000;
|
|
466
429
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const { transaction } = await revenue.monetization.create({
|
|
430
|
+
// Friend 1 pays $40
|
|
431
|
+
await revenue.monetization.create({
|
|
470
432
|
data: {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
referenceId:
|
|
474
|
-
referenceModel: 'Order',
|
|
433
|
+
customerId: friend1,
|
|
434
|
+
organizationId: restaurantId,
|
|
435
|
+
referenceId: orderId,
|
|
436
|
+
referenceModel: 'Order',
|
|
475
437
|
},
|
|
476
|
-
|
|
477
|
-
|
|
438
|
+
planKey: 'split_payment',
|
|
439
|
+
monetizationType: 'purchase',
|
|
440
|
+
amount: 4000,
|
|
441
|
+
gateway: 'stripe',
|
|
442
|
+
metadata: { splitGroup: 'dinner_dec_10' },
|
|
478
443
|
});
|
|
479
444
|
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
445
|
+
// Friend 2 pays $35
|
|
446
|
+
await revenue.monetization.create({
|
|
447
|
+
data: {
|
|
448
|
+
customerId: friend2,
|
|
449
|
+
organizationId: restaurantId,
|
|
450
|
+
referenceId: orderId,
|
|
451
|
+
referenceModel: 'Order',
|
|
452
|
+
},
|
|
453
|
+
planKey: 'split_payment',
|
|
454
|
+
monetizationType: 'purchase',
|
|
455
|
+
amount: 3500,
|
|
456
|
+
gateway: 'stripe',
|
|
457
|
+
metadata: { splitGroup: 'dinner_dec_10' },
|
|
484
458
|
});
|
|
485
459
|
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
## Hooks
|
|
494
|
-
|
|
495
|
-
```javascript
|
|
496
|
-
const revenue = createRevenue({
|
|
497
|
-
models: { Transaction },
|
|
498
|
-
hooks: {
|
|
499
|
-
// Monetization lifecycle (specific)
|
|
500
|
-
'purchase.created': async ({ transaction, isFree }) => {
|
|
501
|
-
console.log('One-time purchase:', transaction._id);
|
|
502
|
-
},
|
|
503
|
-
'subscription.created': async ({ transaction, isFree }) => {
|
|
504
|
-
console.log('Recurring subscription:', transaction._id);
|
|
505
|
-
},
|
|
506
|
-
'free.created': async ({ transaction }) => {
|
|
507
|
-
console.log('Free access granted:', transaction._id);
|
|
508
|
-
},
|
|
509
|
-
|
|
510
|
-
// Generic event (fires for all types)
|
|
511
|
-
'monetization.created': async ({ transaction, monetizationType }) => {
|
|
512
|
-
console.log(`${monetizationType} created:`, transaction._id);
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
// Payment lifecycle
|
|
516
|
-
'payment.verified': async ({ transaction }) => {
|
|
517
|
-
// Send confirmation email
|
|
518
|
-
},
|
|
519
|
-
'payment.failed': async ({ transaction, error, provider }) => {
|
|
520
|
-
// Alert admin or send customer notification
|
|
521
|
-
console.error('Payment failed:', error);
|
|
522
|
-
},
|
|
523
|
-
'payment.refunded': async ({ refundTransaction }) => {
|
|
524
|
-
// Process refund notification
|
|
525
|
-
},
|
|
526
|
-
|
|
527
|
-
// Subscription management (requires Subscription model)
|
|
528
|
-
'subscription.activated': async ({ subscription }) => {
|
|
529
|
-
// Subscription activated after payment
|
|
530
|
-
},
|
|
531
|
-
'subscription.renewed': async ({ subscription, renewalCount }) => {
|
|
532
|
-
// Subscription renewed
|
|
533
|
-
},
|
|
534
|
-
'subscription.paused': async ({ subscription }) => {
|
|
535
|
-
// Subscription paused
|
|
536
|
-
},
|
|
537
|
-
'subscription.resumed': async ({ subscription }) => {
|
|
538
|
-
// Subscription resumed
|
|
539
|
-
},
|
|
540
|
-
'subscription.cancelled': async ({ subscription }) => {
|
|
541
|
-
// Subscription cancelled
|
|
542
|
-
},
|
|
460
|
+
// Friend 3 pays $25
|
|
461
|
+
await revenue.monetization.create({
|
|
462
|
+
data: {
|
|
463
|
+
customerId: friend3,
|
|
464
|
+
organizationId: restaurantId,
|
|
465
|
+
referenceId: orderId,
|
|
466
|
+
referenceModel: 'Order',
|
|
543
467
|
},
|
|
468
|
+
planKey: 'split_payment',
|
|
469
|
+
monetizationType: 'purchase',
|
|
470
|
+
amount: 2500,
|
|
471
|
+
gateway: 'stripe',
|
|
472
|
+
metadata: { splitGroup: 'dinner_dec_10' },
|
|
544
473
|
});
|
|
545
474
|
```
|
|
546
475
|
|
|
547
|
-
|
|
476
|
+
### Check Payment Status
|
|
548
477
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
**Payment Events:**
|
|
556
|
-
- `payment.verified` - Payment confirmed
|
|
557
|
-
- `payment.failed` - Payment verification failed
|
|
558
|
-
- `payment.refunded` - Refund processed
|
|
559
|
-
- `payment.webhook.{type}` - Webhook events from providers
|
|
560
|
-
|
|
561
|
-
**Subscription Management Events (requires Subscription model):**
|
|
562
|
-
- `subscription.activated`, `subscription.renewed`
|
|
563
|
-
- `subscription.paused`, `subscription.resumed`, `subscription.cancelled`
|
|
478
|
+
```typescript
|
|
479
|
+
// Get all contributions for an order
|
|
480
|
+
const contributions = await Transaction.find({
|
|
481
|
+
referenceId: orderId,
|
|
482
|
+
referenceModel: 'Order',
|
|
483
|
+
});
|
|
564
484
|
|
|
565
|
-
|
|
485
|
+
// Calculate totals
|
|
486
|
+
const verified = contributions.filter(t => t.status === 'verified');
|
|
487
|
+
const totalPaid = verified.reduce((sum, t) => sum + t.amount, 0);
|
|
488
|
+
const remaining = orderTotal - totalPaid;
|
|
489
|
+
const isFullyPaid = totalPaid >= orderTotal;
|
|
490
|
+
|
|
491
|
+
console.log({
|
|
492
|
+
totalPaid, // 10000
|
|
493
|
+
remaining, // 0
|
|
494
|
+
isFullyPaid, // true
|
|
495
|
+
payers: verified.map(t => ({
|
|
496
|
+
customerId: t.customerId,
|
|
497
|
+
amount: t.amount,
|
|
498
|
+
paidAt: t.verifiedAt,
|
|
499
|
+
})),
|
|
500
|
+
});
|
|
501
|
+
```
|
|
566
502
|
|
|
567
|
-
|
|
503
|
+
### Query by Split Group
|
|
568
504
|
|
|
569
|
-
|
|
505
|
+
```typescript
|
|
506
|
+
// Find all payments in a split group
|
|
507
|
+
const groupPayments = await Transaction.find({
|
|
508
|
+
'metadata.splitGroup': 'dinner_dec_10',
|
|
509
|
+
});
|
|
570
510
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
511
|
+
// Pending payers
|
|
512
|
+
const pending = await Transaction.find({
|
|
513
|
+
referenceId: orderId,
|
|
514
|
+
status: 'pending',
|
|
515
|
+
});
|
|
516
|
+
```
|
|
577
517
|
|
|
578
|
-
|
|
518
|
+
---
|
|
579
519
|
|
|
580
520
|
## Building Custom Providers
|
|
581
521
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
import { PaymentProvider, PaymentIntent, PaymentResult } from '@classytic/revenue';
|
|
522
|
+
```typescript
|
|
523
|
+
import { PaymentProvider, PaymentIntent, PaymentResult, RefundResult, WebhookEvent } from '@classytic/revenue';
|
|
524
|
+
import type { CreateIntentParams, ProviderCapabilities } from '@classytic/revenue';
|
|
586
525
|
|
|
587
526
|
export class StripeProvider extends PaymentProvider {
|
|
588
|
-
|
|
527
|
+
public override readonly name = 'stripe';
|
|
528
|
+
private stripe: Stripe;
|
|
529
|
+
|
|
530
|
+
constructor(config: { apiKey: string }) {
|
|
589
531
|
super(config);
|
|
590
|
-
this.name = 'stripe';
|
|
591
532
|
this.stripe = new Stripe(config.apiKey);
|
|
592
533
|
}
|
|
593
534
|
|
|
594
|
-
async createIntent(params) {
|
|
535
|
+
async createIntent(params: CreateIntentParams): Promise<PaymentIntent> {
|
|
595
536
|
const intent = await this.stripe.paymentIntents.create({
|
|
596
537
|
amount: params.amount,
|
|
597
|
-
currency: params.currency,
|
|
538
|
+
currency: params.currency ?? 'usd',
|
|
539
|
+
metadata: params.metadata,
|
|
598
540
|
});
|
|
599
541
|
|
|
600
542
|
return new PaymentIntent({
|
|
601
543
|
id: intent.id,
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
provider:
|
|
544
|
+
paymentIntentId: intent.id,
|
|
545
|
+
sessionId: null,
|
|
546
|
+
provider: this.name,
|
|
605
547
|
status: intent.status,
|
|
606
548
|
amount: intent.amount,
|
|
607
549
|
currency: intent.currency,
|
|
608
|
-
clientSecret: intent.client_secret
|
|
609
|
-
|
|
550
|
+
clientSecret: intent.client_secret!,
|
|
551
|
+
metadata: params.metadata ?? {},
|
|
610
552
|
});
|
|
611
553
|
}
|
|
612
554
|
|
|
613
|
-
async verifyPayment(intentId) {
|
|
555
|
+
async verifyPayment(intentId: string): Promise<PaymentResult> {
|
|
614
556
|
const intent = await this.stripe.paymentIntents.retrieve(intentId);
|
|
615
557
|
return new PaymentResult({
|
|
616
558
|
id: intent.id,
|
|
617
|
-
provider:
|
|
559
|
+
provider: this.name,
|
|
618
560
|
status: intent.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
619
|
-
|
|
620
|
-
|
|
561
|
+
amount: intent.amount,
|
|
562
|
+
currency: intent.currency,
|
|
563
|
+
paidAt: intent.status === 'succeeded' ? new Date() : undefined,
|
|
564
|
+
metadata: {},
|
|
621
565
|
});
|
|
622
566
|
}
|
|
623
567
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
**See:** [`docs/guides/PROVIDER_GUIDE.md`](../docs/guides/PROVIDER_GUIDE.md) for complete guide.
|
|
629
|
-
|
|
630
|
-
## TypeScript
|
|
568
|
+
async getStatus(intentId: string): Promise<PaymentResult> {
|
|
569
|
+
return this.verifyPayment(intentId);
|
|
570
|
+
}
|
|
631
571
|
|
|
632
|
-
|
|
572
|
+
async refund(paymentId: string, amount?: number | null): Promise<RefundResult> {
|
|
573
|
+
const refund = await this.stripe.refunds.create({
|
|
574
|
+
payment_intent: paymentId,
|
|
575
|
+
amount: amount ?? undefined,
|
|
576
|
+
});
|
|
633
577
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
578
|
+
return new RefundResult({
|
|
579
|
+
id: refund.id,
|
|
580
|
+
provider: this.name,
|
|
581
|
+
status: refund.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
582
|
+
amount: refund.amount,
|
|
583
|
+
currency: refund.currency,
|
|
584
|
+
refundedAt: new Date(),
|
|
585
|
+
metadata: {},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
637
588
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
589
|
+
async handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent> {
|
|
590
|
+
const sig = headers?.['stripe-signature'];
|
|
591
|
+
const event = this.stripe.webhooks.constructEvent(
|
|
592
|
+
payload as string,
|
|
593
|
+
sig!,
|
|
594
|
+
this.config.webhookSecret as string
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
return new WebhookEvent({
|
|
598
|
+
id: event.id,
|
|
599
|
+
provider: this.name,
|
|
600
|
+
type: event.type,
|
|
601
|
+
data: event.data.object as any,
|
|
602
|
+
createdAt: new Date(event.created * 1000),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
641
605
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
606
|
+
override getCapabilities(): ProviderCapabilities {
|
|
607
|
+
return {
|
|
608
|
+
supportsWebhooks: true,
|
|
609
|
+
supportsRefunds: true,
|
|
610
|
+
supportsPartialRefunds: true,
|
|
611
|
+
requiresManualVerification: false,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
645
615
|
```
|
|
646
616
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
- [`examples/single-tenant.js`](examples/single-tenant.js) - Simple SaaS (no organizations)
|
|
650
|
-
- [`examples/transaction.model.js`](examples/transaction.model.js) - Complete model setup
|
|
651
|
-
- [`examples/complete-flow.js`](examples/complete-flow.js) - Full lifecycle (types, refs, state)
|
|
652
|
-
- [`examples/commission-tracking.js`](examples/commission-tracking.js) - Commission calculation
|
|
653
|
-
- [`examples/hooks-v0.2.0.js`](examples/hooks-v0.2.0.js) - v0.2.0 semantic hooks (NEW)
|
|
617
|
+
---
|
|
654
618
|
|
|
655
619
|
## Error Handling
|
|
656
620
|
|
|
657
|
-
```
|
|
658
|
-
import {
|
|
621
|
+
```typescript
|
|
622
|
+
import {
|
|
623
|
+
RevenueError,
|
|
659
624
|
TransactionNotFoundError,
|
|
660
|
-
ProviderNotFoundError,
|
|
661
625
|
AlreadyVerifiedError,
|
|
662
626
|
RefundError,
|
|
627
|
+
ProviderNotFoundError,
|
|
628
|
+
ValidationError,
|
|
629
|
+
isRevenueError,
|
|
630
|
+
isRetryable,
|
|
663
631
|
} from '@classytic/revenue';
|
|
664
632
|
|
|
665
633
|
try {
|
|
666
634
|
await revenue.payments.verify(id);
|
|
667
635
|
} catch (error) {
|
|
668
636
|
if (error instanceof AlreadyVerifiedError) {
|
|
669
|
-
console.log('Already verified');
|
|
637
|
+
console.log('Already verified:', error.metadata.transactionId);
|
|
670
638
|
} else if (error instanceof TransactionNotFoundError) {
|
|
671
|
-
console.log('
|
|
639
|
+
console.log('Not found');
|
|
640
|
+
} else if (isRevenueError(error) && isRetryable(error)) {
|
|
641
|
+
// Retry the operation
|
|
672
642
|
}
|
|
673
643
|
}
|
|
674
644
|
```
|
|
675
645
|
|
|
676
|
-
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## TypeScript
|
|
649
|
+
|
|
650
|
+
Full TypeScript support with exported types:
|
|
677
651
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
652
|
+
```typescript
|
|
653
|
+
import type {
|
|
654
|
+
Revenue,
|
|
655
|
+
TransactionDocument,
|
|
656
|
+
SubscriptionDocument,
|
|
657
|
+
PaymentProviderInterface,
|
|
658
|
+
CreateIntentParams,
|
|
659
|
+
ProviderCapabilities,
|
|
660
|
+
RevenueEvents,
|
|
661
|
+
MonetizationCreateParams,
|
|
662
|
+
} from '@classytic/revenue';
|
|
663
|
+
```
|
|
681
664
|
|
|
682
|
-
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Testing
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
# Run all tests (75 tests)
|
|
671
|
+
npm test
|
|
672
|
+
|
|
673
|
+
# Run integration tests (requires MongoDB)
|
|
674
|
+
npm test -- tests/integration/
|
|
675
|
+
|
|
676
|
+
# Watch mode
|
|
677
|
+
npm run test:watch
|
|
678
|
+
|
|
679
|
+
# Coverage
|
|
680
|
+
npm run test:coverage
|
|
681
|
+
```
|
|
683
682
|
|
|
684
|
-
|
|
685
|
-
- **Issues**: [Report bugs](https://github.com/classytic/revenue/issues)
|
|
686
|
-
- **NPM**: [@classytic/revenue](https://www.npmjs.com/package/@classytic/revenue)
|
|
683
|
+
---
|
|
687
684
|
|
|
688
685
|
## License
|
|
689
686
|
|