@classytic/revenue 0.0.1 → 0.0.21
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 +190 -2
- package/enums/index.d.ts +107 -0
- package/package.json +17 -5
- package/revenue.d.ts +26 -20
- package/schemas/index.d.ts +33 -0
- package/services/payment.service.js +8 -8
- package/services/subscription.service.js +49 -17
- package/utils/category-resolver.js +74 -0
- package/utils/index.d.ts +99 -0
- package/utils/logger.js +1 -1
- package/providers/manual.js +0 -171
package/README.md
CHANGED
|
@@ -21,6 +21,51 @@ Thin, focused, production-ready library with smart defaults. Built for SaaS, mar
|
|
|
21
21
|
npm install @classytic/revenue
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## Core Concepts
|
|
25
|
+
|
|
26
|
+
### Monetization Types (Strict)
|
|
27
|
+
The library supports **3 monetization types** (strict):
|
|
28
|
+
- **FREE**: No payment required
|
|
29
|
+
- **SUBSCRIPTION**: Recurring payments
|
|
30
|
+
- **PURCHASE**: One-time payments
|
|
31
|
+
|
|
32
|
+
### Transaction Categories (Flexible)
|
|
33
|
+
You can use **custom category names** for your business logic while using the strict monetization types:
|
|
34
|
+
- `'order_subscription'` for subscription orders
|
|
35
|
+
- `'gym_membership'` for gym memberships
|
|
36
|
+
- `'course_enrollment'` for course enrollments
|
|
37
|
+
- Or any custom names you need
|
|
38
|
+
|
|
39
|
+
### How It Works
|
|
40
|
+
```javascript
|
|
41
|
+
const revenue = createRevenue({
|
|
42
|
+
models: { Transaction },
|
|
43
|
+
config: {
|
|
44
|
+
categoryMappings: {
|
|
45
|
+
Order: 'order_subscription', // Customer orders
|
|
46
|
+
PlatformSubscription: 'platform_subscription', // Tenant/org subscriptions
|
|
47
|
+
Membership: 'gym_membership', // User memberships
|
|
48
|
+
Enrollment: 'course_enrollment', // Course enrollments
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// All these use SUBSCRIPTION monetization type but different categories
|
|
54
|
+
await revenue.subscriptions.create({
|
|
55
|
+
entity: 'Order', // Logical identifier → maps to 'order_subscription'
|
|
56
|
+
monetizationType: 'subscription',
|
|
57
|
+
// ...
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await revenue.subscriptions.create({
|
|
61
|
+
entity: 'PlatformSubscription', // Logical identifier → maps to 'platform_subscription'
|
|
62
|
+
monetizationType: 'subscription',
|
|
63
|
+
// ...
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Note:** `entity` is NOT a database model name - it's just a logical identifier you choose to organize your business logic.
|
|
68
|
+
|
|
24
69
|
## Transaction Model Setup
|
|
25
70
|
|
|
26
71
|
Spread library enums/schemas into your Transaction model:
|
|
@@ -37,9 +82,13 @@ import {
|
|
|
37
82
|
paymentDetailsSchema,
|
|
38
83
|
} from '@classytic/revenue/schemas';
|
|
39
84
|
|
|
40
|
-
// Merge library categories with your
|
|
85
|
+
// Merge library categories with your custom ones
|
|
41
86
|
const MY_CATEGORIES = {
|
|
42
|
-
...LIBRARY_CATEGORIES,
|
|
87
|
+
...LIBRARY_CATEGORIES, // subscription, purchase (library defaults)
|
|
88
|
+
ORDER_SUBSCRIPTION: 'order_subscription',
|
|
89
|
+
ORDER_PURCHASE: 'order_purchase',
|
|
90
|
+
GYM_MEMBERSHIP: 'gym_membership',
|
|
91
|
+
COURSE_ENROLLMENT: 'course_enrollment',
|
|
43
92
|
SALARY: 'salary',
|
|
44
93
|
RENT: 'rent',
|
|
45
94
|
EQUIPMENT: 'equipment',
|
|
@@ -91,6 +140,145 @@ const { subscription, transaction } = await revenue.subscriptions.create({
|
|
|
91
140
|
|
|
92
141
|
That's it! The package works immediately with sensible defaults.
|
|
93
142
|
|
|
143
|
+
## Real-World Use Cases
|
|
144
|
+
|
|
145
|
+
### E-commerce Platform with Multiple Order Types
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// Setup
|
|
149
|
+
const revenue = createRevenue({
|
|
150
|
+
models: { Transaction },
|
|
151
|
+
config: {
|
|
152
|
+
categoryMappings: {
|
|
153
|
+
Order: 'order_subscription', // Recurring orders (meal kits, subscriptions)
|
|
154
|
+
Purchase: 'order_purchase', // One-time orders
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Subscription order (meal kit subscription)
|
|
160
|
+
const { subscription, transaction } = await revenue.subscriptions.create({
|
|
161
|
+
data: { organizationId, customerId },
|
|
162
|
+
entity: 'Order', // Logical identifier
|
|
163
|
+
monetizationType: 'subscription', // STRICT: Must be subscription/purchase/free
|
|
164
|
+
planKey: 'monthly',
|
|
165
|
+
amount: 49.99,
|
|
166
|
+
metadata: { productType: 'meal_kit' }
|
|
167
|
+
});
|
|
168
|
+
// Transaction created with category: 'order_subscription'
|
|
169
|
+
|
|
170
|
+
// One-time purchase order
|
|
171
|
+
const { transaction } = await revenue.subscriptions.create({
|
|
172
|
+
data: { organizationId, customerId },
|
|
173
|
+
entity: 'Purchase', // Logical identifier
|
|
174
|
+
monetizationType: 'purchase',
|
|
175
|
+
amount: 99.99,
|
|
176
|
+
metadata: { productType: 'electronics' }
|
|
177
|
+
});
|
|
178
|
+
// Transaction created with category: 'order_purchase'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Gym Management System
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// Setup
|
|
185
|
+
const revenue = createRevenue({
|
|
186
|
+
models: { Transaction },
|
|
187
|
+
config: {
|
|
188
|
+
categoryMappings: {
|
|
189
|
+
Membership: 'gym_membership',
|
|
190
|
+
PersonalTraining: 'personal_training',
|
|
191
|
+
DayPass: 'day_pass',
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Monthly gym membership
|
|
197
|
+
await revenue.subscriptions.create({
|
|
198
|
+
entity: 'Membership',
|
|
199
|
+
monetizationType: 'subscription',
|
|
200
|
+
planKey: 'monthly',
|
|
201
|
+
amount: 59.99,
|
|
202
|
+
});
|
|
203
|
+
// Transaction: 'gym_membership'
|
|
204
|
+
|
|
205
|
+
// Personal training package (one-time purchase)
|
|
206
|
+
await revenue.subscriptions.create({
|
|
207
|
+
entity: 'PersonalTraining',
|
|
208
|
+
monetizationType: 'purchase',
|
|
209
|
+
amount: 299.99,
|
|
210
|
+
});
|
|
211
|
+
// Transaction: 'personal_training'
|
|
212
|
+
|
|
213
|
+
// Day pass (free trial)
|
|
214
|
+
await revenue.subscriptions.create({
|
|
215
|
+
entity: 'DayPass',
|
|
216
|
+
monetizationType: 'free',
|
|
217
|
+
amount: 0,
|
|
218
|
+
});
|
|
219
|
+
// No transaction created for free
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Online Learning Platform
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// Setup
|
|
226
|
+
const revenue = createRevenue({
|
|
227
|
+
models: { Transaction },
|
|
228
|
+
config: {
|
|
229
|
+
categoryMappings: {
|
|
230
|
+
CourseEnrollment: 'course_enrollment',
|
|
231
|
+
MembershipPlan: 'membership_plan',
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// One-time course purchase
|
|
237
|
+
await revenue.subscriptions.create({
|
|
238
|
+
entity: 'CourseEnrollment',
|
|
239
|
+
monetizationType: 'purchase',
|
|
240
|
+
amount: 99.00,
|
|
241
|
+
metadata: { courseId: 'react-advanced' }
|
|
242
|
+
});
|
|
243
|
+
// Transaction: 'course_enrollment'
|
|
244
|
+
|
|
245
|
+
// Monthly all-access membership
|
|
246
|
+
await revenue.subscriptions.create({
|
|
247
|
+
entity: 'MembershipPlan',
|
|
248
|
+
monetizationType: 'subscription',
|
|
249
|
+
planKey: 'monthly',
|
|
250
|
+
amount: 29.99,
|
|
251
|
+
});
|
|
252
|
+
// Transaction: 'membership_plan'
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Without Category Mappings (Defaults)
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// No mappings defined - uses library defaults
|
|
259
|
+
const revenue = createRevenue({
|
|
260
|
+
models: { Transaction },
|
|
261
|
+
config: {
|
|
262
|
+
categoryMappings: {} // Empty or omit this
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// All subscriptions use default 'subscription' category
|
|
267
|
+
await revenue.subscriptions.create({
|
|
268
|
+
monetizationType: 'subscription',
|
|
269
|
+
planKey: 'monthly',
|
|
270
|
+
amount: 49.99,
|
|
271
|
+
});
|
|
272
|
+
// Transaction created with category: 'subscription' (library default)
|
|
273
|
+
|
|
274
|
+
// All purchases use default 'purchase' category
|
|
275
|
+
await revenue.subscriptions.create({
|
|
276
|
+
monetizationType: 'purchase',
|
|
277
|
+
amount: 99.99,
|
|
278
|
+
});
|
|
279
|
+
// Transaction created with category: 'purchase' (library default)
|
|
280
|
+
```
|
|
281
|
+
|
|
94
282
|
## Usage Examples
|
|
95
283
|
|
|
96
284
|
### With Payment Provider
|
package/enums/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @classytic/revenue/enums
|
|
3
|
+
* Centralized Enums
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============ TRANSACTION ENUMS ============
|
|
7
|
+
|
|
8
|
+
export const TRANSACTION_STATUS: {
|
|
9
|
+
readonly PENDING: 'pending';
|
|
10
|
+
readonly PAYMENT_INITIATED: 'payment_initiated';
|
|
11
|
+
readonly PROCESSING: 'processing';
|
|
12
|
+
readonly REQUIRES_ACTION: 'requires_action';
|
|
13
|
+
readonly VERIFIED: 'verified';
|
|
14
|
+
readonly COMPLETED: 'completed';
|
|
15
|
+
readonly FAILED: 'failed';
|
|
16
|
+
readonly CANCELLED: 'cancelled';
|
|
17
|
+
readonly EXPIRED: 'expired';
|
|
18
|
+
readonly REFUNDED: 'refunded';
|
|
19
|
+
readonly PARTIALLY_REFUNDED: 'partially_refunded';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TRANSACTION_STATUS_VALUES: string[];
|
|
23
|
+
|
|
24
|
+
export const LIBRARY_CATEGORIES: {
|
|
25
|
+
readonly SUBSCRIPTION: 'subscription';
|
|
26
|
+
readonly PURCHASE: 'purchase';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const LIBRARY_CATEGORY_VALUES: string[];
|
|
30
|
+
|
|
31
|
+
// ============ PAYMENT ENUMS ============
|
|
32
|
+
|
|
33
|
+
export const PAYMENT_STATUS: {
|
|
34
|
+
readonly PENDING: 'pending';
|
|
35
|
+
readonly VERIFIED: 'verified';
|
|
36
|
+
readonly FAILED: 'failed';
|
|
37
|
+
readonly REFUNDED: 'refunded';
|
|
38
|
+
readonly CANCELLED: 'cancelled';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const PAYMENT_STATUS_VALUES: string[];
|
|
42
|
+
|
|
43
|
+
export const PAYMENT_GATEWAY_TYPE: {
|
|
44
|
+
readonly MANUAL: 'manual';
|
|
45
|
+
readonly STRIPE: 'stripe';
|
|
46
|
+
readonly SSLCOMMERZ: 'sslcommerz';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const PAYMENT_GATEWAY_TYPE_VALUES: string[];
|
|
50
|
+
|
|
51
|
+
// Backward compatibility aliases
|
|
52
|
+
export const GATEWAY_TYPES: typeof PAYMENT_GATEWAY_TYPE;
|
|
53
|
+
export const GATEWAY_TYPE_VALUES: typeof PAYMENT_GATEWAY_TYPE_VALUES;
|
|
54
|
+
|
|
55
|
+
// ============ SUBSCRIPTION ENUMS ============
|
|
56
|
+
|
|
57
|
+
export const SUBSCRIPTION_STATUS: {
|
|
58
|
+
readonly ACTIVE: 'active';
|
|
59
|
+
readonly PAUSED: 'paused';
|
|
60
|
+
readonly CANCELLED: 'cancelled';
|
|
61
|
+
readonly EXPIRED: 'expired';
|
|
62
|
+
readonly PENDING: 'pending';
|
|
63
|
+
readonly INACTIVE: 'inactive';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const SUBSCRIPTION_STATUS_VALUES: string[];
|
|
67
|
+
|
|
68
|
+
export const PLAN_KEYS: {
|
|
69
|
+
readonly MONTHLY: 'monthly';
|
|
70
|
+
readonly QUARTERLY: 'quarterly';
|
|
71
|
+
readonly YEARLY: 'yearly';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const PLAN_KEY_VALUES: string[];
|
|
75
|
+
|
|
76
|
+
// ============ MONETIZATION ENUMS ============
|
|
77
|
+
|
|
78
|
+
export const MONETIZATION_TYPES: {
|
|
79
|
+
readonly FREE: 'free';
|
|
80
|
+
readonly PURCHASE: 'purchase';
|
|
81
|
+
readonly SUBSCRIPTION: 'subscription';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const MONETIZATION_TYPE_VALUES: string[];
|
|
85
|
+
|
|
86
|
+
// ============ DEFAULT EXPORT ============
|
|
87
|
+
|
|
88
|
+
declare const _default: {
|
|
89
|
+
TRANSACTION_STATUS: typeof TRANSACTION_STATUS;
|
|
90
|
+
TRANSACTION_STATUS_VALUES: typeof TRANSACTION_STATUS_VALUES;
|
|
91
|
+
LIBRARY_CATEGORIES: typeof LIBRARY_CATEGORIES;
|
|
92
|
+
LIBRARY_CATEGORY_VALUES: typeof LIBRARY_CATEGORY_VALUES;
|
|
93
|
+
PAYMENT_STATUS: typeof PAYMENT_STATUS;
|
|
94
|
+
PAYMENT_STATUS_VALUES: typeof PAYMENT_STATUS_VALUES;
|
|
95
|
+
PAYMENT_GATEWAY_TYPE: typeof PAYMENT_GATEWAY_TYPE;
|
|
96
|
+
PAYMENT_GATEWAY_TYPE_VALUES: typeof PAYMENT_GATEWAY_TYPE_VALUES;
|
|
97
|
+
GATEWAY_TYPES: typeof GATEWAY_TYPES;
|
|
98
|
+
GATEWAY_TYPE_VALUES: typeof GATEWAY_TYPE_VALUES;
|
|
99
|
+
SUBSCRIPTION_STATUS: typeof SUBSCRIPTION_STATUS;
|
|
100
|
+
SUBSCRIPTION_STATUS_VALUES: typeof SUBSCRIPTION_STATUS_VALUES;
|
|
101
|
+
PLAN_KEYS: typeof PLAN_KEYS;
|
|
102
|
+
PLAN_KEY_VALUES: typeof PLAN_KEY_VALUES;
|
|
103
|
+
MONETIZATION_TYPES: typeof MONETIZATION_TYPES;
|
|
104
|
+
MONETIZATION_TYPE_VALUES: typeof MONETIZATION_TYPE_VALUES;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default _default;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/revenue",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Enterprise revenue management system with subscriptions, purchases, proration, and payment processing",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "revenue.d.ts",
|
|
@@ -39,10 +39,22 @@
|
|
|
39
39
|
"node": ">=18.0.0"
|
|
40
40
|
},
|
|
41
41
|
"exports": {
|
|
42
|
-
".":
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./revenue.d.ts",
|
|
44
|
+
"default": "./index.js"
|
|
45
|
+
},
|
|
46
|
+
"./enums": {
|
|
47
|
+
"types": "./enums/index.d.ts",
|
|
48
|
+
"default": "./enums/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./schemas": {
|
|
51
|
+
"types": "./schemas/index.d.ts",
|
|
52
|
+
"default": "./schemas/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./utils": {
|
|
55
|
+
"types": "./utils/index.d.ts",
|
|
56
|
+
"default": "./utils/index.js"
|
|
57
|
+
}
|
|
46
58
|
},
|
|
47
59
|
"files": [
|
|
48
60
|
"index.js",
|
package/revenue.d.ts
CHANGED
|
@@ -105,13 +105,21 @@ export class SubscriptionService {
|
|
|
105
105
|
amount: number;
|
|
106
106
|
currency?: string;
|
|
107
107
|
gateway?: string;
|
|
108
|
+
entity?: string;
|
|
109
|
+
monetizationType?: 'free' | 'subscription' | 'purchase';
|
|
108
110
|
paymentData?: any;
|
|
109
111
|
metadata?: Record<string, any>;
|
|
110
112
|
idempotencyKey?: string;
|
|
111
113
|
}): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent | null }>;
|
|
112
114
|
|
|
113
115
|
activate(subscriptionId: string, options?: { timestamp?: Date }): Promise<any>;
|
|
114
|
-
renew(subscriptionId: string, params?:
|
|
116
|
+
renew(subscriptionId: string, params?: {
|
|
117
|
+
gateway?: string;
|
|
118
|
+
entity?: string;
|
|
119
|
+
paymentData?: any;
|
|
120
|
+
metadata?: Record<string, any>;
|
|
121
|
+
idempotencyKey?: string;
|
|
122
|
+
}): Promise<{ subscription: any; transaction: any; paymentIntent: PaymentIntent }>;
|
|
115
123
|
cancel(subscriptionId: string, options?: { immediate?: boolean; reason?: string }): Promise<any>;
|
|
116
124
|
pause(subscriptionId: string, options?: { reason?: string }): Promise<any>;
|
|
117
125
|
resume(subscriptionId: string, options?: { extendPeriod?: boolean }): Promise<any>;
|
|
@@ -193,7 +201,23 @@ export interface RevenueOptions {
|
|
|
193
201
|
providers?: Record<string, PaymentProvider>;
|
|
194
202
|
hooks?: Record<string, Function[]>;
|
|
195
203
|
config?: {
|
|
196
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Maps logical entity identifiers to custom transaction category names
|
|
206
|
+
*
|
|
207
|
+
* Entity identifiers are NOT database model names - they are logical identifiers
|
|
208
|
+
* you choose to organize your business logic.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* categoryMappings: {
|
|
212
|
+
* Order: 'order_subscription', // Customer orders
|
|
213
|
+
* PlatformSubscription: 'platform_subscription', // Tenant/org subscriptions
|
|
214
|
+
* TenantUpgrade: 'tenant_upgrade', // Tenant upgrades
|
|
215
|
+
* Membership: 'gym_membership', // User memberships
|
|
216
|
+
* Enrollment: 'course_enrollment', // Course enrollments
|
|
217
|
+
* }
|
|
218
|
+
*
|
|
219
|
+
* If not specified, falls back to library defaults: 'subscription' or 'purchase'
|
|
220
|
+
*/
|
|
197
221
|
categoryMappings?: Record<string, string>;
|
|
198
222
|
[key: string]: any;
|
|
199
223
|
};
|
|
@@ -218,19 +242,10 @@ export const TRANSACTION_STATUS: {
|
|
|
218
242
|
PARTIALLY_REFUNDED: 'partially_refunded';
|
|
219
243
|
};
|
|
220
244
|
|
|
221
|
-
export const TRANSACTION_TYPES: {
|
|
222
|
-
INCOME: 'income';
|
|
223
|
-
EXPENSE: 'expense';
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// Note: PAYMENT_METHOD removed - users define their own payment methods
|
|
227
|
-
|
|
228
245
|
export const PAYMENT_GATEWAY_TYPE: {
|
|
229
246
|
MANUAL: 'manual';
|
|
230
247
|
STRIPE: 'stripe';
|
|
231
248
|
SSLCOMMERZ: 'sslcommerz';
|
|
232
|
-
BKASH_GATEWAY: 'bkash_gateway';
|
|
233
|
-
NAGAD_GATEWAY: 'nagad_gateway';
|
|
234
249
|
};
|
|
235
250
|
|
|
236
251
|
export const SUBSCRIPTION_STATUS: {
|
|
@@ -263,15 +278,6 @@ export const subscriptionPlanSchema: Schema;
|
|
|
263
278
|
export const gatewaySchema: Schema;
|
|
264
279
|
export const commissionSchema: Schema;
|
|
265
280
|
export const paymentDetailsSchema: Schema;
|
|
266
|
-
export const tenantSnapshotSchema: Schema;
|
|
267
|
-
export const timelineEventSchema: Schema;
|
|
268
|
-
export const customerInfoSchema: Schema;
|
|
269
|
-
export const customDiscountSchema: Schema;
|
|
270
|
-
export const stripeAccountSchema: Schema;
|
|
271
|
-
export const sslcommerzAccountSchema: Schema;
|
|
272
|
-
export const bkashMerchantSchema: Schema;
|
|
273
|
-
export const bankAccountSchema: Schema;
|
|
274
|
-
export const walletSchema: Schema;
|
|
275
281
|
|
|
276
282
|
// ============ UTILITIES ============
|
|
277
283
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @classytic/revenue/schemas
|
|
3
|
+
* Core schemas for injection into your models
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Schema } from 'mongoose';
|
|
7
|
+
|
|
8
|
+
// ============ TRANSACTION SCHEMAS ============
|
|
9
|
+
|
|
10
|
+
export const currentPaymentSchema: Schema;
|
|
11
|
+
export const paymentSummarySchema: Schema;
|
|
12
|
+
export const paymentDetailsSchema: Schema;
|
|
13
|
+
export const gatewaySchema: Schema;
|
|
14
|
+
export const commissionSchema: Schema;
|
|
15
|
+
|
|
16
|
+
// ============ SUBSCRIPTION SCHEMAS ============
|
|
17
|
+
|
|
18
|
+
export const subscriptionInfoSchema: Schema;
|
|
19
|
+
export const subscriptionPlanSchema: Schema;
|
|
20
|
+
|
|
21
|
+
// ============ DEFAULT EXPORT ============
|
|
22
|
+
|
|
23
|
+
declare const _default: {
|
|
24
|
+
currentPaymentSchema: Schema;
|
|
25
|
+
paymentSummarySchema: Schema;
|
|
26
|
+
paymentDetailsSchema: Schema;
|
|
27
|
+
gatewaySchema: Schema;
|
|
28
|
+
commissionSchema: Schema;
|
|
29
|
+
subscriptionInfoSchema: Schema;
|
|
30
|
+
subscriptionPlanSchema: Schema;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default _default;
|
|
@@ -132,7 +132,7 @@ export class PaymentService {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (!transaction) {
|
|
135
|
-
throw new
|
|
135
|
+
throw new TransactionNotFoundError(paymentIntentId);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// Get provider
|
|
@@ -140,7 +140,7 @@ export class PaymentService {
|
|
|
140
140
|
const provider = this.providers[gatewayType];
|
|
141
141
|
|
|
142
142
|
if (!provider) {
|
|
143
|
-
throw new
|
|
143
|
+
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Get status from provider
|
|
@@ -218,7 +218,7 @@ export class PaymentService {
|
|
|
218
218
|
refundResult = await provider.refund(paymentId, refundAmount, { reason });
|
|
219
219
|
} catch (error) {
|
|
220
220
|
this.logger.error('Refund failed:', error);
|
|
221
|
-
throw new
|
|
221
|
+
throw new RefundError(paymentId, error.message);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// Update transaction
|
|
@@ -262,7 +262,7 @@ export class PaymentService {
|
|
|
262
262
|
const provider = this.providers[providerName];
|
|
263
263
|
|
|
264
264
|
if (!provider) {
|
|
265
|
-
throw new
|
|
265
|
+
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
// Process webhook via provider
|
|
@@ -271,7 +271,7 @@ export class PaymentService {
|
|
|
271
271
|
webhookEvent = await provider.handleWebhook(payload, headers);
|
|
272
272
|
} catch (error) {
|
|
273
273
|
this.logger.error('Webhook processing failed:', error);
|
|
274
|
-
throw new
|
|
274
|
+
throw new ProviderError(providerName, `Webhook processing failed: ${error.message}`);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Find transaction by payment intent ID from webhook
|
|
@@ -286,7 +286,7 @@ export class PaymentService {
|
|
|
286
286
|
eventId: webhookEvent.id,
|
|
287
287
|
paymentIntentId: webhookEvent.data.paymentIntentId,
|
|
288
288
|
});
|
|
289
|
-
throw new
|
|
289
|
+
throw new TransactionNotFoundError(webhookEvent.data.paymentIntentId);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// Check for duplicate webhook processing (idempotency)
|
|
@@ -368,7 +368,7 @@ export class PaymentService {
|
|
|
368
368
|
const transaction = await TransactionModel.findById(transactionId);
|
|
369
369
|
|
|
370
370
|
if (!transaction) {
|
|
371
|
-
throw new
|
|
371
|
+
throw new TransactionNotFoundError(transactionId);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
return transaction;
|
|
@@ -383,7 +383,7 @@ export class PaymentService {
|
|
|
383
383
|
getProvider(providerName) {
|
|
384
384
|
const provider = this.providers[providerName];
|
|
385
385
|
if (!provider) {
|
|
386
|
-
throw new
|
|
386
|
+
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
387
387
|
}
|
|
388
388
|
return provider;
|
|
389
389
|
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
PaymentIntentCreationError,
|
|
18
18
|
} from '../core/errors.js';
|
|
19
19
|
import { triggerHook } from '../utils/hooks.js';
|
|
20
|
+
import { resolveCategory } from '../utils/category-resolver.js';
|
|
21
|
+
import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Subscription Service
|
|
@@ -41,6 +43,9 @@ export class SubscriptionService {
|
|
|
41
43
|
* @param {Number} params.amount - Subscription amount
|
|
42
44
|
* @param {String} params.currency - Currency code (default: 'BDT')
|
|
43
45
|
* @param {String} params.gateway - Payment gateway to use (default: 'manual')
|
|
46
|
+
* @param {String} params.entity - Logical entity identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
|
|
47
|
+
* NOTE: This is NOT a database model name - it's just a logical identifier for categoryMappings
|
|
48
|
+
* @param {String} params.monetizationType - Monetization type ('free', 'subscription', 'purchase')
|
|
44
49
|
* @param {Object} params.paymentData - Payment method details
|
|
45
50
|
* @param {Object} params.metadata - Additional metadata
|
|
46
51
|
* @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
|
|
@@ -53,6 +58,8 @@ export class SubscriptionService {
|
|
|
53
58
|
amount,
|
|
54
59
|
currency = 'BDT',
|
|
55
60
|
gateway = 'manual',
|
|
61
|
+
entity = null,
|
|
62
|
+
monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
|
|
56
63
|
paymentData,
|
|
57
64
|
metadata = {},
|
|
58
65
|
idempotencyKey = null,
|
|
@@ -99,6 +106,9 @@ export class SubscriptionService {
|
|
|
99
106
|
throw new PaymentIntentCreationError(gateway, error);
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
// Resolve category based on entity and monetizationType
|
|
110
|
+
const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
|
|
111
|
+
|
|
102
112
|
// Create transaction record
|
|
103
113
|
const TransactionModel = this.models.Transaction;
|
|
104
114
|
transaction = await TransactionModel.create({
|
|
@@ -106,7 +116,7 @@ export class SubscriptionService {
|
|
|
106
116
|
customerId: data.customerId || null,
|
|
107
117
|
amount,
|
|
108
118
|
currency,
|
|
109
|
-
category
|
|
119
|
+
category,
|
|
110
120
|
type: 'credit',
|
|
111
121
|
status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
|
|
112
122
|
gateway: {
|
|
@@ -121,6 +131,8 @@ export class SubscriptionService {
|
|
|
121
131
|
metadata: {
|
|
122
132
|
...metadata,
|
|
123
133
|
planKey,
|
|
134
|
+
entity,
|
|
135
|
+
monetizationType,
|
|
124
136
|
paymentIntentId: paymentIntent.id,
|
|
125
137
|
},
|
|
126
138
|
idempotencyKey: idempotencyKey || `sub_${nanoid(16)}`,
|
|
@@ -146,6 +158,8 @@ export class SubscriptionService {
|
|
|
146
158
|
metadata: {
|
|
147
159
|
...metadata,
|
|
148
160
|
isFree,
|
|
161
|
+
entity,
|
|
162
|
+
monetizationType,
|
|
149
163
|
},
|
|
150
164
|
...data,
|
|
151
165
|
});
|
|
@@ -218,35 +232,41 @@ export class SubscriptionService {
|
|
|
218
232
|
*
|
|
219
233
|
* @param {String} subscriptionId - Subscription ID
|
|
220
234
|
* @param {Object} params - Renewal parameters
|
|
235
|
+
* @param {String} params.gateway - Payment gateway to use (default: 'manual')
|
|
236
|
+
* @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
|
|
237
|
+
* @param {Object} params.paymentData - Payment method details
|
|
238
|
+
* @param {Object} params.metadata - Additional metadata
|
|
239
|
+
* @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
|
|
221
240
|
* @returns {Promise<Object>} { subscription, transaction, paymentIntent }
|
|
222
241
|
*/
|
|
223
242
|
async renew(subscriptionId, params = {}) {
|
|
224
243
|
const {
|
|
225
244
|
gateway = 'manual',
|
|
245
|
+
entity = null,
|
|
226
246
|
paymentData,
|
|
227
247
|
metadata = {},
|
|
228
248
|
idempotencyKey = null,
|
|
229
249
|
} = params;
|
|
230
250
|
|
|
231
251
|
if (!this.models.Subscription) {
|
|
232
|
-
throw new
|
|
252
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
const SubscriptionModel = this.models.Subscription;
|
|
236
256
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
237
257
|
|
|
238
258
|
if (!subscription) {
|
|
239
|
-
throw new
|
|
259
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
240
260
|
}
|
|
241
261
|
|
|
242
262
|
if (subscription.amount === 0) {
|
|
243
|
-
throw new
|
|
263
|
+
throw new InvalidAmountError(0, 'Free subscriptions do not require renewal');
|
|
244
264
|
}
|
|
245
265
|
|
|
246
266
|
// Get provider
|
|
247
267
|
const provider = this.providers[gateway];
|
|
248
268
|
if (!provider) {
|
|
249
|
-
throw new
|
|
269
|
+
throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
// Create payment intent
|
|
@@ -260,6 +280,11 @@ export class SubscriptionService {
|
|
|
260
280
|
},
|
|
261
281
|
});
|
|
262
282
|
|
|
283
|
+
// Resolve category - use provided entity or inherit from subscription metadata
|
|
284
|
+
const effectiveEntity = entity || subscription.metadata?.entity;
|
|
285
|
+
const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
|
|
286
|
+
const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
|
|
287
|
+
|
|
263
288
|
// Create transaction
|
|
264
289
|
const TransactionModel = this.models.Transaction;
|
|
265
290
|
const transaction = await TransactionModel.create({
|
|
@@ -267,7 +292,7 @@ export class SubscriptionService {
|
|
|
267
292
|
customerId: subscription.customerId,
|
|
268
293
|
amount: subscription.amount,
|
|
269
294
|
currency: subscription.currency || 'BDT',
|
|
270
|
-
category
|
|
295
|
+
category,
|
|
271
296
|
type: 'credit',
|
|
272
297
|
status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
|
|
273
298
|
gateway: {
|
|
@@ -282,6 +307,8 @@ export class SubscriptionService {
|
|
|
282
307
|
metadata: {
|
|
283
308
|
...metadata,
|
|
284
309
|
subscriptionId: subscription._id.toString(),
|
|
310
|
+
entity: effectiveEntity,
|
|
311
|
+
monetizationType: effectiveMonetizationType,
|
|
285
312
|
isRenewal: true,
|
|
286
313
|
paymentIntentId: paymentIntent.id,
|
|
287
314
|
},
|
|
@@ -322,14 +349,14 @@ export class SubscriptionService {
|
|
|
322
349
|
const { immediate = false, reason = null } = options;
|
|
323
350
|
|
|
324
351
|
if (!this.models.Subscription) {
|
|
325
|
-
throw new
|
|
352
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
326
353
|
}
|
|
327
354
|
|
|
328
355
|
const SubscriptionModel = this.models.Subscription;
|
|
329
356
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
330
357
|
|
|
331
358
|
if (!subscription) {
|
|
332
|
-
throw new
|
|
359
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
333
360
|
}
|
|
334
361
|
|
|
335
362
|
const now = new Date();
|
|
@@ -369,18 +396,18 @@ export class SubscriptionService {
|
|
|
369
396
|
const { reason = null } = options;
|
|
370
397
|
|
|
371
398
|
if (!this.models.Subscription) {
|
|
372
|
-
throw new
|
|
399
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
373
400
|
}
|
|
374
401
|
|
|
375
402
|
const SubscriptionModel = this.models.Subscription;
|
|
376
403
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
377
404
|
|
|
378
405
|
if (!subscription) {
|
|
379
|
-
throw new
|
|
406
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
380
407
|
}
|
|
381
408
|
|
|
382
409
|
if (!subscription.isActive) {
|
|
383
|
-
throw new
|
|
410
|
+
throw new SubscriptionNotActiveError(subscriptionId, 'Only active subscriptions can be paused');
|
|
384
411
|
}
|
|
385
412
|
|
|
386
413
|
const pausedAt = new Date();
|
|
@@ -412,18 +439,23 @@ export class SubscriptionService {
|
|
|
412
439
|
const { extendPeriod = false } = options;
|
|
413
440
|
|
|
414
441
|
if (!this.models.Subscription) {
|
|
415
|
-
throw new
|
|
442
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
416
443
|
}
|
|
417
444
|
|
|
418
445
|
const SubscriptionModel = this.models.Subscription;
|
|
419
446
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
420
447
|
|
|
421
448
|
if (!subscription) {
|
|
422
|
-
throw new
|
|
449
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
423
450
|
}
|
|
424
451
|
|
|
425
452
|
if (!subscription.pausedAt) {
|
|
426
|
-
throw new
|
|
453
|
+
throw new InvalidStateTransitionError(
|
|
454
|
+
'resume',
|
|
455
|
+
'paused',
|
|
456
|
+
subscription.status,
|
|
457
|
+
'Only paused subscriptions can be resumed'
|
|
458
|
+
);
|
|
427
459
|
}
|
|
428
460
|
|
|
429
461
|
const now = new Date();
|
|
@@ -463,7 +495,7 @@ export class SubscriptionService {
|
|
|
463
495
|
*/
|
|
464
496
|
async list(filters = {}, options = {}) {
|
|
465
497
|
if (!this.models.Subscription) {
|
|
466
|
-
throw new
|
|
498
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
467
499
|
}
|
|
468
500
|
|
|
469
501
|
const SubscriptionModel = this.models.Subscription;
|
|
@@ -486,14 +518,14 @@ export class SubscriptionService {
|
|
|
486
518
|
*/
|
|
487
519
|
async get(subscriptionId) {
|
|
488
520
|
if (!this.models.Subscription) {
|
|
489
|
-
throw new
|
|
521
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
490
522
|
}
|
|
491
523
|
|
|
492
524
|
const SubscriptionModel = this.models.Subscription;
|
|
493
525
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
494
526
|
|
|
495
527
|
if (!subscription) {
|
|
496
|
-
throw new
|
|
528
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
497
529
|
}
|
|
498
530
|
|
|
499
531
|
return subscription;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category Resolver Utility
|
|
3
|
+
* @classytic/revenue
|
|
4
|
+
*
|
|
5
|
+
* Resolves transaction category based on referenceModel and categoryMappings
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LIBRARY_CATEGORIES } from '../enums/transaction.enums.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve category for a transaction based on entity and monetizationType
|
|
12
|
+
*
|
|
13
|
+
* Resolution Logic:
|
|
14
|
+
* 1. If categoryMappings[entity] exists → use it
|
|
15
|
+
* 2. Otherwise → fall back to default library category
|
|
16
|
+
*
|
|
17
|
+
* @param {String} entity - The logical entity/identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
|
|
18
|
+
* NOTE: This is NOT a database model name - it's just a logical identifier
|
|
19
|
+
* @param {String} monetizationType - The monetization type ('subscription', 'purchase', 'free')
|
|
20
|
+
* @param {Object} categoryMappings - User-defined category mappings from config
|
|
21
|
+
* @returns {String} Category name for the transaction
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // With mapping defined
|
|
25
|
+
* resolveCategory('Order', 'subscription', { Order: 'order_subscription' })
|
|
26
|
+
* // Returns: 'order_subscription'
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Without mapping, falls back to library default
|
|
30
|
+
* resolveCategory('Order', 'subscription', {})
|
|
31
|
+
* // Returns: 'subscription'
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Different entities with different mappings
|
|
35
|
+
* const mappings = {
|
|
36
|
+
* Order: 'order_subscription',
|
|
37
|
+
* PlatformSubscription: 'platform_subscription',
|
|
38
|
+
* TenantUpgrade: 'tenant_upgrade',
|
|
39
|
+
* Membership: 'gym_membership',
|
|
40
|
+
* Enrollment: 'course_enrollment',
|
|
41
|
+
* };
|
|
42
|
+
* resolveCategory('PlatformSubscription', 'subscription', mappings)
|
|
43
|
+
* // Returns: 'platform_subscription'
|
|
44
|
+
*/
|
|
45
|
+
export function resolveCategory(entity, monetizationType, categoryMappings = {}) {
|
|
46
|
+
// If user has defined a custom mapping for this entity, use it
|
|
47
|
+
if (entity && categoryMappings[entity]) {
|
|
48
|
+
return categoryMappings[entity];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Otherwise, fall back to library default based on monetization type
|
|
52
|
+
switch (monetizationType) {
|
|
53
|
+
case 'subscription':
|
|
54
|
+
return LIBRARY_CATEGORIES.SUBSCRIPTION; // 'subscription'
|
|
55
|
+
case 'purchase':
|
|
56
|
+
return LIBRARY_CATEGORIES.PURCHASE; // 'purchase'
|
|
57
|
+
default:
|
|
58
|
+
return LIBRARY_CATEGORIES.SUBSCRIPTION; // Default to subscription
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate that a category is defined in user's Transaction model enum
|
|
64
|
+
* This is informational - actual validation happens at Mongoose schema level
|
|
65
|
+
*
|
|
66
|
+
* @param {String} category - Category to validate
|
|
67
|
+
* @param {Array<String>} allowedCategories - List of allowed categories
|
|
68
|
+
* @returns {Boolean} Whether category is valid
|
|
69
|
+
*/
|
|
70
|
+
export function isCategoryValid(category, allowedCategories = []) {
|
|
71
|
+
return allowedCategories.includes(category);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default resolveCategory;
|
package/utils/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @classytic/revenue/utils
|
|
3
|
+
* Core utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============ TRANSACTION TYPE UTILITIES ============
|
|
7
|
+
|
|
8
|
+
export const TRANSACTION_TYPE: {
|
|
9
|
+
readonly MONETIZATION: 'monetization';
|
|
10
|
+
readonly MANUAL: 'manual';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const PROTECTED_MONETIZATION_FIELDS: readonly string[];
|
|
14
|
+
export const EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: readonly string[];
|
|
15
|
+
export const MANUAL_TRANSACTION_CREATE_FIELDS: readonly string[];
|
|
16
|
+
export const MANUAL_TRANSACTION_UPDATE_FIELDS: readonly string[];
|
|
17
|
+
|
|
18
|
+
export interface TransactionTypeOptions {
|
|
19
|
+
targetModels?: string[];
|
|
20
|
+
additionalCategories?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isMonetizationTransaction(
|
|
24
|
+
transaction: any,
|
|
25
|
+
options?: TransactionTypeOptions
|
|
26
|
+
): boolean;
|
|
27
|
+
|
|
28
|
+
export function isManualTransaction(
|
|
29
|
+
transaction: any,
|
|
30
|
+
options?: TransactionTypeOptions
|
|
31
|
+
): boolean;
|
|
32
|
+
|
|
33
|
+
export function getTransactionType(
|
|
34
|
+
transaction: any,
|
|
35
|
+
options?: TransactionTypeOptions
|
|
36
|
+
): 'monetization' | 'manual';
|
|
37
|
+
|
|
38
|
+
export function getAllowedUpdateFields(
|
|
39
|
+
transaction: any,
|
|
40
|
+
options?: TransactionTypeOptions
|
|
41
|
+
): string[];
|
|
42
|
+
|
|
43
|
+
export interface FieldValidationResult {
|
|
44
|
+
allowed: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateFieldUpdate(
|
|
49
|
+
transaction: any,
|
|
50
|
+
fieldName: string,
|
|
51
|
+
options?: TransactionTypeOptions
|
|
52
|
+
): FieldValidationResult;
|
|
53
|
+
|
|
54
|
+
export function canSelfVerify(
|
|
55
|
+
transaction: any,
|
|
56
|
+
options?: TransactionTypeOptions
|
|
57
|
+
): boolean;
|
|
58
|
+
|
|
59
|
+
// ============ LOGGER UTILITIES ============
|
|
60
|
+
|
|
61
|
+
export interface Logger {
|
|
62
|
+
info(...args: any[]): void;
|
|
63
|
+
warn(...args: any[]): void;
|
|
64
|
+
error(...args: any[]): void;
|
|
65
|
+
debug(...args: any[]): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const logger: Logger;
|
|
69
|
+
export function setLogger(logger: Logger | Console): void;
|
|
70
|
+
|
|
71
|
+
// ============ HOOK UTILITIES ============
|
|
72
|
+
|
|
73
|
+
export function triggerHook(
|
|
74
|
+
hooks: Record<string, Function[]>,
|
|
75
|
+
event: string,
|
|
76
|
+
data: any,
|
|
77
|
+
logger: Logger
|
|
78
|
+
): void;
|
|
79
|
+
|
|
80
|
+
// ============ DEFAULT EXPORT ============
|
|
81
|
+
|
|
82
|
+
declare const _default: {
|
|
83
|
+
TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
|
|
84
|
+
isMonetizationTransaction: typeof isMonetizationTransaction;
|
|
85
|
+
isManualTransaction: typeof isManualTransaction;
|
|
86
|
+
getTransactionType: typeof getTransactionType;
|
|
87
|
+
PROTECTED_MONETIZATION_FIELDS: typeof PROTECTED_MONETIZATION_FIELDS;
|
|
88
|
+
EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION: typeof EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION;
|
|
89
|
+
MANUAL_TRANSACTION_CREATE_FIELDS: typeof MANUAL_TRANSACTION_CREATE_FIELDS;
|
|
90
|
+
MANUAL_TRANSACTION_UPDATE_FIELDS: typeof MANUAL_TRANSACTION_UPDATE_FIELDS;
|
|
91
|
+
getAllowedUpdateFields: typeof getAllowedUpdateFields;
|
|
92
|
+
validateFieldUpdate: typeof validateFieldUpdate;
|
|
93
|
+
canSelfVerify: typeof canSelfVerify;
|
|
94
|
+
logger: typeof logger;
|
|
95
|
+
setLogger: typeof setLogger;
|
|
96
|
+
triggerHook: typeof triggerHook;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default _default;
|
package/utils/logger.js
CHANGED
package/providers/manual.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manual Payment Provider
|
|
3
|
-
* @classytic/revenue
|
|
4
|
-
*
|
|
5
|
-
* Built-in provider for manual payment verification
|
|
6
|
-
* Perfect for: Cash, bank transfers, mobile money without API
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { PaymentProvider, PaymentIntent, PaymentResult, RefundResult } from './base.js';
|
|
10
|
-
import { nanoid } from 'nanoid';
|
|
11
|
-
|
|
12
|
-
export class ManualProvider extends PaymentProvider {
|
|
13
|
-
constructor(config = {}) {
|
|
14
|
-
super(config);
|
|
15
|
-
this.name = 'manual';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create manual payment intent
|
|
20
|
-
* Returns instructions for manual payment
|
|
21
|
-
*/
|
|
22
|
-
async createIntent(params) {
|
|
23
|
-
const intentId = `manual_${nanoid(16)}`;
|
|
24
|
-
|
|
25
|
-
return new PaymentIntent({
|
|
26
|
-
id: intentId,
|
|
27
|
-
provider: 'manual',
|
|
28
|
-
status: 'pending',
|
|
29
|
-
amount: params.amount,
|
|
30
|
-
currency: params.currency || 'BDT',
|
|
31
|
-
metadata: params.metadata || {},
|
|
32
|
-
instructions: this._getPaymentInstructions(params),
|
|
33
|
-
raw: params,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Verify manual payment
|
|
39
|
-
* Note: This is called by admin after checking payment proof
|
|
40
|
-
*/
|
|
41
|
-
async verifyPayment(intentId) {
|
|
42
|
-
// Manual verification doesn't auto-verify
|
|
43
|
-
// Admin must explicitly call payment verification endpoint
|
|
44
|
-
return new PaymentResult({
|
|
45
|
-
id: intentId,
|
|
46
|
-
provider: 'manual',
|
|
47
|
-
status: 'requires_manual_approval',
|
|
48
|
-
amount: 0, // Amount will be filled by transaction
|
|
49
|
-
currency: 'BDT',
|
|
50
|
-
metadata: {
|
|
51
|
-
message: 'Manual payment requires admin verification',
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get payment status
|
|
58
|
-
*/
|
|
59
|
-
async getStatus(intentId) {
|
|
60
|
-
return this.verifyPayment(intentId);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Refund manual payment
|
|
65
|
-
*/
|
|
66
|
-
async refund(paymentId, amount, options = {}) {
|
|
67
|
-
const refundId = `refund_${nanoid(16)}`;
|
|
68
|
-
|
|
69
|
-
return new RefundResult({
|
|
70
|
-
id: refundId,
|
|
71
|
-
provider: 'manual',
|
|
72
|
-
status: 'succeeded', // Manual refunds are immediately marked as succeeded
|
|
73
|
-
amount: amount,
|
|
74
|
-
currency: options.currency || 'BDT',
|
|
75
|
-
refundedAt: new Date(),
|
|
76
|
-
reason: options.reason || 'Manual refund',
|
|
77
|
-
metadata: options.metadata || {},
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Manual provider doesn't support webhooks
|
|
83
|
-
*/
|
|
84
|
-
async handleWebhook(payload, headers) {
|
|
85
|
-
throw new Error('Manual provider does not support webhooks');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get provider capabilities
|
|
90
|
-
*/
|
|
91
|
-
getCapabilities() {
|
|
92
|
-
return {
|
|
93
|
-
supportsWebhooks: false,
|
|
94
|
-
supportsRefunds: true,
|
|
95
|
-
supportsPartialRefunds: true,
|
|
96
|
-
requiresManualVerification: true,
|
|
97
|
-
supportedMethods: ['cash', 'bank', 'bkash', 'nagad', 'rocket', 'manual'],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Generate payment instructions for customer
|
|
103
|
-
* @private
|
|
104
|
-
*/
|
|
105
|
-
_getPaymentInstructions(params) {
|
|
106
|
-
const { organizationPaymentInfo, method } = params.metadata || {};
|
|
107
|
-
|
|
108
|
-
if (!organizationPaymentInfo) {
|
|
109
|
-
return 'Please contact the organization for payment details.';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const instructions = [];
|
|
113
|
-
|
|
114
|
-
// Add method-specific instructions
|
|
115
|
-
switch (method) {
|
|
116
|
-
case 'bkash':
|
|
117
|
-
case 'nagad':
|
|
118
|
-
case 'rocket':
|
|
119
|
-
if (organizationPaymentInfo[`${method}Number`]) {
|
|
120
|
-
instructions.push(
|
|
121
|
-
`Send money via ${method.toUpperCase()}:`,
|
|
122
|
-
`Number: ${organizationPaymentInfo[`${method}Number`]}`,
|
|
123
|
-
`Amount: ${params.amount} ${params.currency || 'BDT'}`,
|
|
124
|
-
``,
|
|
125
|
-
`After payment, provide the transaction ID/reference number.`
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
case 'bank':
|
|
131
|
-
if (organizationPaymentInfo.bankAccount) {
|
|
132
|
-
const bank = organizationPaymentInfo.bankAccount;
|
|
133
|
-
instructions.push(
|
|
134
|
-
`Bank Transfer Details:`,
|
|
135
|
-
`Bank: ${bank.bankName || 'N/A'}`,
|
|
136
|
-
`Account: ${bank.accountNumber || 'N/A'}`,
|
|
137
|
-
`Account Name: ${bank.accountName || 'N/A'}`,
|
|
138
|
-
`Amount: ${params.amount} ${params.currency || 'BDT'}`,
|
|
139
|
-
``,
|
|
140
|
-
`After payment, upload proof and provide reference.`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
|
|
145
|
-
case 'cash':
|
|
146
|
-
instructions.push(
|
|
147
|
-
`Cash Payment:`,
|
|
148
|
-
`Amount: ${params.amount} ${params.currency || 'BDT'}`,
|
|
149
|
-
``,
|
|
150
|
-
`Pay at the organization's office and get a receipt.`
|
|
151
|
-
);
|
|
152
|
-
break;
|
|
153
|
-
|
|
154
|
-
default:
|
|
155
|
-
instructions.push(
|
|
156
|
-
`Payment Amount: ${params.amount} ${params.currency || 'BDT'}`,
|
|
157
|
-
``,
|
|
158
|
-
`Contact the organization for payment details.`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Add custom instructions if provided
|
|
163
|
-
if (organizationPaymentInfo.paymentInstructions) {
|
|
164
|
-
instructions.push(``, `Additional Instructions:`, organizationPaymentInfo.paymentInstructions);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return instructions.join('\n');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export default ManualProvider;
|