@classytic/revenue 0.0.2 → 0.0.22
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 +248 -322
- package/enums/index.d.ts +9 -0
- package/enums/index.js +4 -0
- package/enums/transaction.enums.js +16 -0
- package/package.json +1 -1
- package/revenue.d.ts +51 -21
- package/schemas/index.d.ts +0 -21
- package/services/payment.service.js +41 -10
- package/services/subscription.service.js +66 -19
- package/utils/category-resolver.js +74 -0
- package/utils/logger.js +1 -1
- package/providers/manual.js +0 -171
package/enums/index.d.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
// ============ TRANSACTION ENUMS ============
|
|
7
7
|
|
|
8
|
+
export const TRANSACTION_TYPE: {
|
|
9
|
+
readonly INCOME: 'income';
|
|
10
|
+
readonly EXPENSE: 'expense';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TRANSACTION_TYPE_VALUES: string[];
|
|
14
|
+
|
|
8
15
|
export const TRANSACTION_STATUS: {
|
|
9
16
|
readonly PENDING: 'pending';
|
|
10
17
|
readonly PAYMENT_INITIATED: 'payment_initiated';
|
|
@@ -86,6 +93,8 @@ export const MONETIZATION_TYPE_VALUES: string[];
|
|
|
86
93
|
// ============ DEFAULT EXPORT ============
|
|
87
94
|
|
|
88
95
|
declare const _default: {
|
|
96
|
+
TRANSACTION_TYPE: typeof TRANSACTION_TYPE;
|
|
97
|
+
TRANSACTION_TYPE_VALUES: typeof TRANSACTION_TYPE_VALUES;
|
|
89
98
|
TRANSACTION_STATUS: typeof TRANSACTION_STATUS;
|
|
90
99
|
TRANSACTION_STATUS_VALUES: typeof TRANSACTION_STATUS_VALUES;
|
|
91
100
|
LIBRARY_CATEGORIES: typeof LIBRARY_CATEGORIES;
|
package/enums/index.js
CHANGED
|
@@ -16,6 +16,8 @@ export * from './monetization.enums.js';
|
|
|
16
16
|
|
|
17
17
|
// Default export for convenience
|
|
18
18
|
import {
|
|
19
|
+
TRANSACTION_TYPE,
|
|
20
|
+
TRANSACTION_TYPE_VALUES,
|
|
19
21
|
TRANSACTION_STATUS,
|
|
20
22
|
TRANSACTION_STATUS_VALUES,
|
|
21
23
|
LIBRARY_CATEGORIES,
|
|
@@ -45,6 +47,8 @@ import {
|
|
|
45
47
|
|
|
46
48
|
export default {
|
|
47
49
|
// Transaction enums
|
|
50
|
+
TRANSACTION_TYPE,
|
|
51
|
+
TRANSACTION_TYPE_VALUES,
|
|
48
52
|
TRANSACTION_STATUS,
|
|
49
53
|
TRANSACTION_STATUS_VALUES,
|
|
50
54
|
LIBRARY_CATEGORIES,
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
* Users should define their own categories and merge with these.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
// ============ TRANSACTION TYPE ============
|
|
10
|
+
/**
|
|
11
|
+
* Transaction Type - Income vs Expense
|
|
12
|
+
*
|
|
13
|
+
* INCOME: Money coming in (payments, subscriptions, purchases)
|
|
14
|
+
* EXPENSE: Money going out (refunds, payouts)
|
|
15
|
+
*
|
|
16
|
+
* Users can map these in their config via transactionTypeMapping
|
|
17
|
+
*/
|
|
18
|
+
export const TRANSACTION_TYPE = {
|
|
19
|
+
INCOME: 'income',
|
|
20
|
+
EXPENSE: 'expense',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const TRANSACTION_TYPE_VALUES = Object.values(TRANSACTION_TYPE);
|
|
24
|
+
|
|
9
25
|
// ============ TRANSACTION STATUS ============
|
|
10
26
|
/**
|
|
11
27
|
* Transaction Status - Library-managed states
|
package/package.json
CHANGED
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>;
|
|
@@ -124,7 +132,7 @@ export class PaymentService {
|
|
|
124
132
|
|
|
125
133
|
verify(paymentIntentId: string, options?: { verifiedBy?: string }): Promise<{ transaction: any; paymentResult: PaymentResult; status: string }>;
|
|
126
134
|
getStatus(paymentIntentId: string): Promise<{ transaction: any; paymentResult: PaymentResult; status: string; provider: string }>;
|
|
127
|
-
refund(paymentId: string, amount?: number, options?: { reason?: string }): Promise<{ transaction: any; refundResult: RefundResult; status: string }>;
|
|
135
|
+
refund(paymentId: string, amount?: number, options?: { reason?: string }): Promise<{ transaction: any; refundTransaction: any; refundResult: RefundResult; status: string }>;
|
|
128
136
|
handleWebhook(providerName: string, payload: any, headers?: any): Promise<{ event: WebhookEvent; transaction: any; status: string }>;
|
|
129
137
|
list(filters?: any, options?: any): Promise<any[]>;
|
|
130
138
|
get(transactionId: string): Promise<any>;
|
|
@@ -193,8 +201,43 @@ 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>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Maps transaction types to income/expense for your accounting system
|
|
225
|
+
*
|
|
226
|
+
* Allows you to control how different transaction types are recorded:
|
|
227
|
+
* - 'income': Money coming in (payments, subscriptions)
|
|
228
|
+
* - 'expense': Money going out (refunds)
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* transactionTypeMapping: {
|
|
232
|
+
* subscription: 'income',
|
|
233
|
+
* subscription_renewal: 'income',
|
|
234
|
+
* purchase: 'income',
|
|
235
|
+
* refund: 'expense',
|
|
236
|
+
* }
|
|
237
|
+
*
|
|
238
|
+
* If not specified, library defaults to 'income' for all payment transactions
|
|
239
|
+
*/
|
|
240
|
+
transactionTypeMapping?: Record<string, 'income' | 'expense'>;
|
|
198
241
|
[key: string]: any;
|
|
199
242
|
};
|
|
200
243
|
logger?: Console | any;
|
|
@@ -204,6 +247,11 @@ export function createRevenue(options: RevenueOptions): Revenue;
|
|
|
204
247
|
|
|
205
248
|
// ============ ENUMS ============
|
|
206
249
|
|
|
250
|
+
export const TRANSACTION_TYPE: {
|
|
251
|
+
INCOME: 'income';
|
|
252
|
+
EXPENSE: 'expense';
|
|
253
|
+
};
|
|
254
|
+
|
|
207
255
|
export const TRANSACTION_STATUS: {
|
|
208
256
|
PENDING: 'pending';
|
|
209
257
|
PAYMENT_INITIATED: 'payment_initiated';
|
|
@@ -218,19 +266,10 @@ export const TRANSACTION_STATUS: {
|
|
|
218
266
|
PARTIALLY_REFUNDED: 'partially_refunded';
|
|
219
267
|
};
|
|
220
268
|
|
|
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
269
|
export const PAYMENT_GATEWAY_TYPE: {
|
|
229
270
|
MANUAL: 'manual';
|
|
230
271
|
STRIPE: 'stripe';
|
|
231
272
|
SSLCOMMERZ: 'sslcommerz';
|
|
232
|
-
BKASH_GATEWAY: 'bkash_gateway';
|
|
233
|
-
NAGAD_GATEWAY: 'nagad_gateway';
|
|
234
273
|
};
|
|
235
274
|
|
|
236
275
|
export const SUBSCRIPTION_STATUS: {
|
|
@@ -263,15 +302,6 @@ export const subscriptionPlanSchema: Schema;
|
|
|
263
302
|
export const gatewaySchema: Schema;
|
|
264
303
|
export const commissionSchema: Schema;
|
|
265
304
|
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
305
|
|
|
276
306
|
// ============ UTILITIES ============
|
|
277
307
|
|
package/schemas/index.d.ts
CHANGED
|
@@ -12,23 +12,11 @@ export const paymentSummarySchema: Schema;
|
|
|
12
12
|
export const paymentDetailsSchema: Schema;
|
|
13
13
|
export const gatewaySchema: Schema;
|
|
14
14
|
export const commissionSchema: Schema;
|
|
15
|
-
export const tenantSnapshotSchema: Schema;
|
|
16
|
-
export const timelineEventSchema: Schema;
|
|
17
|
-
export const customerInfoSchema: Schema;
|
|
18
15
|
|
|
19
16
|
// ============ SUBSCRIPTION SCHEMAS ============
|
|
20
17
|
|
|
21
18
|
export const subscriptionInfoSchema: Schema;
|
|
22
19
|
export const subscriptionPlanSchema: Schema;
|
|
23
|
-
export const customDiscountSchema: Schema;
|
|
24
|
-
|
|
25
|
-
// ============ GATEWAY ACCOUNT SCHEMAS ============
|
|
26
|
-
|
|
27
|
-
export const stripeAccountSchema: Schema;
|
|
28
|
-
export const sslcommerzAccountSchema: Schema;
|
|
29
|
-
export const bkashMerchantSchema: Schema;
|
|
30
|
-
export const bankAccountSchema: Schema;
|
|
31
|
-
export const walletSchema: Schema;
|
|
32
20
|
|
|
33
21
|
// ============ DEFAULT EXPORT ============
|
|
34
22
|
|
|
@@ -38,17 +26,8 @@ declare const _default: {
|
|
|
38
26
|
paymentDetailsSchema: Schema;
|
|
39
27
|
gatewaySchema: Schema;
|
|
40
28
|
commissionSchema: Schema;
|
|
41
|
-
tenantSnapshotSchema: Schema;
|
|
42
|
-
timelineEventSchema: Schema;
|
|
43
|
-
customerInfoSchema: Schema;
|
|
44
29
|
subscriptionInfoSchema: Schema;
|
|
45
30
|
subscriptionPlanSchema: Schema;
|
|
46
|
-
customDiscountSchema: Schema;
|
|
47
|
-
stripeAccountSchema: Schema;
|
|
48
|
-
sslcommerzAccountSchema: Schema;
|
|
49
|
-
bkashMerchantSchema: Schema;
|
|
50
|
-
bankAccountSchema: Schema;
|
|
51
|
-
walletSchema: Schema;
|
|
52
31
|
};
|
|
53
32
|
|
|
54
33
|
export default _default;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ProviderCapabilityError,
|
|
17
17
|
} from '../core/errors.js';
|
|
18
18
|
import { triggerHook } from '../utils/hooks.js';
|
|
19
|
+
import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Payment Service
|
|
@@ -132,7 +133,7 @@ export class PaymentService {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
if (!transaction) {
|
|
135
|
-
throw new
|
|
136
|
+
throw new TransactionNotFoundError(paymentIntentId);
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// Get provider
|
|
@@ -140,7 +141,7 @@ export class PaymentService {
|
|
|
140
141
|
const provider = this.providers[gatewayType];
|
|
141
142
|
|
|
142
143
|
if (!provider) {
|
|
143
|
-
throw new
|
|
144
|
+
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// Get status from provider
|
|
@@ -218,18 +219,46 @@ export class PaymentService {
|
|
|
218
219
|
refundResult = await provider.refund(paymentId, refundAmount, { reason });
|
|
219
220
|
} catch (error) {
|
|
220
221
|
this.logger.error('Refund failed:', error);
|
|
221
|
-
throw new
|
|
222
|
+
throw new RefundError(paymentId, error.message);
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
//
|
|
225
|
+
// Create separate refund transaction (EXPENSE) for proper accounting
|
|
226
|
+
const refundTransactionType = this.config.transactionTypeMapping?.refund || TRANSACTION_TYPE.EXPENSE;
|
|
227
|
+
|
|
228
|
+
const refundTransaction = await TransactionModel.create({
|
|
229
|
+
organizationId: transaction.organizationId,
|
|
230
|
+
customerId: transaction.customerId,
|
|
231
|
+
amount: refundAmount,
|
|
232
|
+
currency: transaction.currency,
|
|
233
|
+
category: transaction.category,
|
|
234
|
+
type: refundTransactionType, // EXPENSE - money going out
|
|
235
|
+
method: transaction.method || 'manual',
|
|
236
|
+
status: 'completed',
|
|
237
|
+
gateway: {
|
|
238
|
+
type: transaction.gateway?.type || 'manual',
|
|
239
|
+
paymentIntentId: refundResult.id,
|
|
240
|
+
provider: refundResult.provider,
|
|
241
|
+
},
|
|
242
|
+
paymentDetails: transaction.paymentDetails,
|
|
243
|
+
metadata: {
|
|
244
|
+
...transaction.metadata,
|
|
245
|
+
isRefund: true,
|
|
246
|
+
originalTransactionId: transaction._id.toString(),
|
|
247
|
+
refundReason: reason,
|
|
248
|
+
refundResult: refundResult.metadata,
|
|
249
|
+
},
|
|
250
|
+
idempotencyKey: `refund_${transaction._id}_${Date.now()}`,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Update original transaction status
|
|
225
254
|
const isPartialRefund = refundAmount < transaction.amount;
|
|
226
255
|
transaction.status = isPartialRefund ? 'partially_refunded' : 'refunded';
|
|
227
256
|
transaction.refundedAmount = (transaction.refundedAmount || 0) + refundAmount;
|
|
228
257
|
transaction.refundedAt = refundResult.refundedAt || new Date();
|
|
229
258
|
transaction.metadata = {
|
|
230
259
|
...transaction.metadata,
|
|
260
|
+
refundTransactionId: refundTransaction._id.toString(),
|
|
231
261
|
refundReason: reason,
|
|
232
|
-
refundResult: refundResult.metadata,
|
|
233
262
|
};
|
|
234
263
|
|
|
235
264
|
await transaction.save();
|
|
@@ -237,6 +266,7 @@ export class PaymentService {
|
|
|
237
266
|
// Trigger hook
|
|
238
267
|
this._triggerHook('payment.refunded', {
|
|
239
268
|
transaction,
|
|
269
|
+
refundTransaction,
|
|
240
270
|
refundResult,
|
|
241
271
|
refundAmount,
|
|
242
272
|
reason,
|
|
@@ -245,6 +275,7 @@ export class PaymentService {
|
|
|
245
275
|
|
|
246
276
|
return {
|
|
247
277
|
transaction,
|
|
278
|
+
refundTransaction,
|
|
248
279
|
refundResult,
|
|
249
280
|
status: transaction.status,
|
|
250
281
|
};
|
|
@@ -262,7 +293,7 @@ export class PaymentService {
|
|
|
262
293
|
const provider = this.providers[providerName];
|
|
263
294
|
|
|
264
295
|
if (!provider) {
|
|
265
|
-
throw new
|
|
296
|
+
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
// Process webhook via provider
|
|
@@ -271,7 +302,7 @@ export class PaymentService {
|
|
|
271
302
|
webhookEvent = await provider.handleWebhook(payload, headers);
|
|
272
303
|
} catch (error) {
|
|
273
304
|
this.logger.error('Webhook processing failed:', error);
|
|
274
|
-
throw new
|
|
305
|
+
throw new ProviderError(providerName, `Webhook processing failed: ${error.message}`);
|
|
275
306
|
}
|
|
276
307
|
|
|
277
308
|
// Find transaction by payment intent ID from webhook
|
|
@@ -286,7 +317,7 @@ export class PaymentService {
|
|
|
286
317
|
eventId: webhookEvent.id,
|
|
287
318
|
paymentIntentId: webhookEvent.data.paymentIntentId,
|
|
288
319
|
});
|
|
289
|
-
throw new
|
|
320
|
+
throw new TransactionNotFoundError(webhookEvent.data.paymentIntentId);
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
// Check for duplicate webhook processing (idempotency)
|
|
@@ -368,7 +399,7 @@ export class PaymentService {
|
|
|
368
399
|
const transaction = await TransactionModel.findById(transactionId);
|
|
369
400
|
|
|
370
401
|
if (!transaction) {
|
|
371
|
-
throw new
|
|
402
|
+
throw new TransactionNotFoundError(transactionId);
|
|
372
403
|
}
|
|
373
404
|
|
|
374
405
|
return transaction;
|
|
@@ -383,7 +414,7 @@ export class PaymentService {
|
|
|
383
414
|
getProvider(providerName) {
|
|
384
415
|
const provider = this.providers[providerName];
|
|
385
416
|
if (!provider) {
|
|
386
|
-
throw new
|
|
417
|
+
throw new ProviderNotFoundError(providerName, Object.keys(this.providers));
|
|
387
418
|
}
|
|
388
419
|
return provider;
|
|
389
420
|
}
|
|
@@ -15,8 +15,12 @@ import {
|
|
|
15
15
|
ModelNotRegisteredError,
|
|
16
16
|
SubscriptionNotActiveError,
|
|
17
17
|
PaymentIntentCreationError,
|
|
18
|
+
InvalidStateTransitionError,
|
|
18
19
|
} from '../core/errors.js';
|
|
19
20
|
import { triggerHook } from '../utils/hooks.js';
|
|
21
|
+
import { resolveCategory } from '../utils/category-resolver.js';
|
|
22
|
+
import { MONETIZATION_TYPES } from '../enums/monetization.enums.js';
|
|
23
|
+
import { TRANSACTION_TYPE } from '../enums/transaction.enums.js';
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Subscription Service
|
|
@@ -41,6 +45,9 @@ export class SubscriptionService {
|
|
|
41
45
|
* @param {Number} params.amount - Subscription amount
|
|
42
46
|
* @param {String} params.currency - Currency code (default: 'BDT')
|
|
43
47
|
* @param {String} params.gateway - Payment gateway to use (default: 'manual')
|
|
48
|
+
* @param {String} params.entity - Logical entity identifier (e.g., 'Order', 'PlatformSubscription', 'Membership')
|
|
49
|
+
* NOTE: This is NOT a database model name - it's just a logical identifier for categoryMappings
|
|
50
|
+
* @param {String} params.monetizationType - Monetization type ('free', 'subscription', 'purchase')
|
|
44
51
|
* @param {Object} params.paymentData - Payment method details
|
|
45
52
|
* @param {Object} params.metadata - Additional metadata
|
|
46
53
|
* @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
|
|
@@ -53,6 +60,8 @@ export class SubscriptionService {
|
|
|
53
60
|
amount,
|
|
54
61
|
currency = 'BDT',
|
|
55
62
|
gateway = 'manual',
|
|
63
|
+
entity = null,
|
|
64
|
+
monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
|
|
56
65
|
paymentData,
|
|
57
66
|
metadata = {},
|
|
58
67
|
idempotencyKey = null,
|
|
@@ -99,6 +108,14 @@ export class SubscriptionService {
|
|
|
99
108
|
throw new PaymentIntentCreationError(gateway, error);
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
// Resolve category based on entity and monetizationType
|
|
112
|
+
const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
|
|
113
|
+
|
|
114
|
+
// Resolve transaction type using config mapping or default to 'income'
|
|
115
|
+
const transactionType = this.config.transactionTypeMapping?.subscription
|
|
116
|
+
|| this.config.transactionTypeMapping?.[monetizationType]
|
|
117
|
+
|| TRANSACTION_TYPE.INCOME;
|
|
118
|
+
|
|
102
119
|
// Create transaction record
|
|
103
120
|
const TransactionModel = this.models.Transaction;
|
|
104
121
|
transaction = await TransactionModel.create({
|
|
@@ -106,8 +123,9 @@ export class SubscriptionService {
|
|
|
106
123
|
customerId: data.customerId || null,
|
|
107
124
|
amount,
|
|
108
125
|
currency,
|
|
109
|
-
category
|
|
110
|
-
type:
|
|
126
|
+
category,
|
|
127
|
+
type: transactionType,
|
|
128
|
+
method: paymentData?.method || 'manual',
|
|
111
129
|
status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
|
|
112
130
|
gateway: {
|
|
113
131
|
type: gateway,
|
|
@@ -121,6 +139,8 @@ export class SubscriptionService {
|
|
|
121
139
|
metadata: {
|
|
122
140
|
...metadata,
|
|
123
141
|
planKey,
|
|
142
|
+
entity,
|
|
143
|
+
monetizationType,
|
|
124
144
|
paymentIntentId: paymentIntent.id,
|
|
125
145
|
},
|
|
126
146
|
idempotencyKey: idempotencyKey || `sub_${nanoid(16)}`,
|
|
@@ -146,6 +166,8 @@ export class SubscriptionService {
|
|
|
146
166
|
metadata: {
|
|
147
167
|
...metadata,
|
|
148
168
|
isFree,
|
|
169
|
+
entity,
|
|
170
|
+
monetizationType,
|
|
149
171
|
},
|
|
150
172
|
...data,
|
|
151
173
|
});
|
|
@@ -218,35 +240,41 @@ export class SubscriptionService {
|
|
|
218
240
|
*
|
|
219
241
|
* @param {String} subscriptionId - Subscription ID
|
|
220
242
|
* @param {Object} params - Renewal parameters
|
|
243
|
+
* @param {String} params.gateway - Payment gateway to use (default: 'manual')
|
|
244
|
+
* @param {String} params.entity - Logical entity identifier (optional, inherits from subscription)
|
|
245
|
+
* @param {Object} params.paymentData - Payment method details
|
|
246
|
+
* @param {Object} params.metadata - Additional metadata
|
|
247
|
+
* @param {String} params.idempotencyKey - Idempotency key for duplicate prevention
|
|
221
248
|
* @returns {Promise<Object>} { subscription, transaction, paymentIntent }
|
|
222
249
|
*/
|
|
223
250
|
async renew(subscriptionId, params = {}) {
|
|
224
251
|
const {
|
|
225
252
|
gateway = 'manual',
|
|
253
|
+
entity = null,
|
|
226
254
|
paymentData,
|
|
227
255
|
metadata = {},
|
|
228
256
|
idempotencyKey = null,
|
|
229
257
|
} = params;
|
|
230
258
|
|
|
231
259
|
if (!this.models.Subscription) {
|
|
232
|
-
throw new
|
|
260
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
233
261
|
}
|
|
234
262
|
|
|
235
263
|
const SubscriptionModel = this.models.Subscription;
|
|
236
264
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
237
265
|
|
|
238
266
|
if (!subscription) {
|
|
239
|
-
throw new
|
|
267
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
240
268
|
}
|
|
241
269
|
|
|
242
270
|
if (subscription.amount === 0) {
|
|
243
|
-
throw new
|
|
271
|
+
throw new InvalidAmountError(0, 'Free subscriptions do not require renewal');
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
// Get provider
|
|
247
275
|
const provider = this.providers[gateway];
|
|
248
276
|
if (!provider) {
|
|
249
|
-
throw new
|
|
277
|
+
throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
|
|
250
278
|
}
|
|
251
279
|
|
|
252
280
|
// Create payment intent
|
|
@@ -260,6 +288,17 @@ export class SubscriptionService {
|
|
|
260
288
|
},
|
|
261
289
|
});
|
|
262
290
|
|
|
291
|
+
// Resolve category - use provided entity or inherit from subscription metadata
|
|
292
|
+
const effectiveEntity = entity || subscription.metadata?.entity;
|
|
293
|
+
const effectiveMonetizationType = subscription.metadata?.monetizationType || MONETIZATION_TYPES.SUBSCRIPTION;
|
|
294
|
+
const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
|
|
295
|
+
|
|
296
|
+
// Resolve transaction type using config mapping or default to 'income'
|
|
297
|
+
const transactionType = this.config.transactionTypeMapping?.subscription_renewal
|
|
298
|
+
|| this.config.transactionTypeMapping?.subscription
|
|
299
|
+
|| this.config.transactionTypeMapping?.[effectiveMonetizationType]
|
|
300
|
+
|| TRANSACTION_TYPE.INCOME;
|
|
301
|
+
|
|
263
302
|
// Create transaction
|
|
264
303
|
const TransactionModel = this.models.Transaction;
|
|
265
304
|
const transaction = await TransactionModel.create({
|
|
@@ -267,8 +306,9 @@ export class SubscriptionService {
|
|
|
267
306
|
customerId: subscription.customerId,
|
|
268
307
|
amount: subscription.amount,
|
|
269
308
|
currency: subscription.currency || 'BDT',
|
|
270
|
-
category
|
|
271
|
-
type:
|
|
309
|
+
category,
|
|
310
|
+
type: transactionType,
|
|
311
|
+
method: paymentData?.method || 'manual',
|
|
272
312
|
status: paymentIntent.status === 'succeeded' ? 'verified' : 'pending',
|
|
273
313
|
gateway: {
|
|
274
314
|
type: gateway,
|
|
@@ -282,6 +322,8 @@ export class SubscriptionService {
|
|
|
282
322
|
metadata: {
|
|
283
323
|
...metadata,
|
|
284
324
|
subscriptionId: subscription._id.toString(),
|
|
325
|
+
entity: effectiveEntity,
|
|
326
|
+
monetizationType: effectiveMonetizationType,
|
|
285
327
|
isRenewal: true,
|
|
286
328
|
paymentIntentId: paymentIntent.id,
|
|
287
329
|
},
|
|
@@ -322,14 +364,14 @@ export class SubscriptionService {
|
|
|
322
364
|
const { immediate = false, reason = null } = options;
|
|
323
365
|
|
|
324
366
|
if (!this.models.Subscription) {
|
|
325
|
-
throw new
|
|
367
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
326
368
|
}
|
|
327
369
|
|
|
328
370
|
const SubscriptionModel = this.models.Subscription;
|
|
329
371
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
330
372
|
|
|
331
373
|
if (!subscription) {
|
|
332
|
-
throw new
|
|
374
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
333
375
|
}
|
|
334
376
|
|
|
335
377
|
const now = new Date();
|
|
@@ -369,18 +411,18 @@ export class SubscriptionService {
|
|
|
369
411
|
const { reason = null } = options;
|
|
370
412
|
|
|
371
413
|
if (!this.models.Subscription) {
|
|
372
|
-
throw new
|
|
414
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
373
415
|
}
|
|
374
416
|
|
|
375
417
|
const SubscriptionModel = this.models.Subscription;
|
|
376
418
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
377
419
|
|
|
378
420
|
if (!subscription) {
|
|
379
|
-
throw new
|
|
421
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
380
422
|
}
|
|
381
423
|
|
|
382
424
|
if (!subscription.isActive) {
|
|
383
|
-
throw new
|
|
425
|
+
throw new SubscriptionNotActiveError(subscriptionId, 'Only active subscriptions can be paused');
|
|
384
426
|
}
|
|
385
427
|
|
|
386
428
|
const pausedAt = new Date();
|
|
@@ -412,18 +454,23 @@ export class SubscriptionService {
|
|
|
412
454
|
const { extendPeriod = false } = options;
|
|
413
455
|
|
|
414
456
|
if (!this.models.Subscription) {
|
|
415
|
-
throw new
|
|
457
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
416
458
|
}
|
|
417
459
|
|
|
418
460
|
const SubscriptionModel = this.models.Subscription;
|
|
419
461
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
420
462
|
|
|
421
463
|
if (!subscription) {
|
|
422
|
-
throw new
|
|
464
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
423
465
|
}
|
|
424
466
|
|
|
425
467
|
if (!subscription.pausedAt) {
|
|
426
|
-
throw new
|
|
468
|
+
throw new InvalidStateTransitionError(
|
|
469
|
+
'resume',
|
|
470
|
+
'paused',
|
|
471
|
+
subscription.status,
|
|
472
|
+
'Only paused subscriptions can be resumed'
|
|
473
|
+
);
|
|
427
474
|
}
|
|
428
475
|
|
|
429
476
|
const now = new Date();
|
|
@@ -463,7 +510,7 @@ export class SubscriptionService {
|
|
|
463
510
|
*/
|
|
464
511
|
async list(filters = {}, options = {}) {
|
|
465
512
|
if (!this.models.Subscription) {
|
|
466
|
-
throw new
|
|
513
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
467
514
|
}
|
|
468
515
|
|
|
469
516
|
const SubscriptionModel = this.models.Subscription;
|
|
@@ -486,14 +533,14 @@ export class SubscriptionService {
|
|
|
486
533
|
*/
|
|
487
534
|
async get(subscriptionId) {
|
|
488
535
|
if (!this.models.Subscription) {
|
|
489
|
-
throw new
|
|
536
|
+
throw new ModelNotRegisteredError('Subscription');
|
|
490
537
|
}
|
|
491
538
|
|
|
492
539
|
const SubscriptionModel = this.models.Subscription;
|
|
493
540
|
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
494
541
|
|
|
495
542
|
if (!subscription) {
|
|
496
|
-
throw new
|
|
543
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
497
544
|
}
|
|
498
545
|
|
|
499
546
|
return subscription;
|