node-paytmpg 7.5.19 → 8.0.4
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/dist/app/controllers/adapters/interfaces.d.ts +10 -0
- package/dist/app/controllers/adapters/interfaces.js +2 -0
- package/dist/app/controllers/adapters/razorpay.d.ts +15 -0
- package/dist/app/controllers/adapters/razorpay.js +61 -0
- package/dist/app/controllers/htmlhelper.d.ts +1 -1
- package/dist/app/controllers/htmlhelper.js +11 -6
- package/dist/app/controllers/payment.controller.js +127 -26
- package/dist/app/controllers/subscription.controller.d.ts +22 -0
- package/dist/app/controllers/subscription.controller.js +400 -0
- package/dist/app/controllers/subscription.webhook.d.ts +4 -0
- package/dist/app/controllers/subscription.webhook.js +190 -0
- package/dist/app/models/index.d.ts +34 -0
- package/dist/app/routes/subscription_route.d.ts +2 -0
- package/dist/app/routes/subscription_route.js +24 -0
- package/dist/index.js +16 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NPConfig, NPPlan, NPSubscription } from "../../models";
|
|
2
|
+
export interface ISubscriptionProvider {
|
|
3
|
+
createPlan(plan: NPPlan, config: NPConfig): Promise<string>;
|
|
4
|
+
createSubscription(sub: NPSubscription, plan: NPPlan, config: NPConfig): Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
url: string;
|
|
7
|
+
}>;
|
|
8
|
+
getSubscription(gatewayId: string, config: NPConfig): Promise<any>;
|
|
9
|
+
cancelSubscription(gatewayId: string, cancelAtCycleEnd: boolean, config: NPConfig): Promise<any>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ISubscriptionProvider } from './interfaces';
|
|
2
|
+
import { NPConfig, NPPlan, NPSubscription } from '../../models';
|
|
3
|
+
/**
|
|
4
|
+
* Only used for subscriptions for now
|
|
5
|
+
*/
|
|
6
|
+
export declare class RazorpayAdapter implements ISubscriptionProvider {
|
|
7
|
+
private getInstance;
|
|
8
|
+
createPlan(plan: NPPlan, config: NPConfig): Promise<string>;
|
|
9
|
+
createSubscription(sub: NPSubscription, plan: NPPlan, config: NPConfig): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}>;
|
|
13
|
+
getSubscription(gatewayId: string, config: NPConfig): Promise<any>;
|
|
14
|
+
cancelSubscription(gatewayId: string, cancelAtCycleEnd: boolean, config: NPConfig): Promise<any>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RazorpayAdapter = void 0;
|
|
7
|
+
const razorpay_1 = __importDefault(require("razorpay"));
|
|
8
|
+
/**
|
|
9
|
+
* Only used for subscriptions for now
|
|
10
|
+
*/
|
|
11
|
+
class RazorpayAdapter {
|
|
12
|
+
getInstance(config) {
|
|
13
|
+
return new razorpay_1.default({
|
|
14
|
+
key_id: config.KEY,
|
|
15
|
+
key_secret: config.SECRET
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async createPlan(plan, config) {
|
|
19
|
+
const instance = this.getInstance(config);
|
|
20
|
+
const payload = {
|
|
21
|
+
period: plan.period,
|
|
22
|
+
interval: plan.interval,
|
|
23
|
+
item: {
|
|
24
|
+
name: plan.name,
|
|
25
|
+
description: plan.description || plan.name,
|
|
26
|
+
amount: plan.amount * 100, // Razorpay takes amount in smallest currency unit (paise)
|
|
27
|
+
currency: plan.currency || 'INR'
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const result = await instance.plans.create(payload);
|
|
31
|
+
return result.id;
|
|
32
|
+
}
|
|
33
|
+
async createSubscription(sub, plan, config) {
|
|
34
|
+
const instance = this.getInstance(config);
|
|
35
|
+
const payload = {
|
|
36
|
+
plan_id: plan.gateway_plan_id,
|
|
37
|
+
total_count: 120, // A reasonably large default total count to act as 'perpetual' unless overwritten, can be customized later
|
|
38
|
+
customer_notify: 1 // Let razorpay handle links
|
|
39
|
+
};
|
|
40
|
+
// Trial Period Implementation
|
|
41
|
+
if (plan.trial_days && plan.trial_days > 0) {
|
|
42
|
+
// start_at should be a unix timestamp (in seconds)
|
|
43
|
+
const trialEndTimestamp = Math.floor(Date.now() / 1000) + (plan.trial_days * 24 * 60 * 60);
|
|
44
|
+
payload.start_at = trialEndTimestamp;
|
|
45
|
+
}
|
|
46
|
+
const result = await instance.subscriptions.create(payload);
|
|
47
|
+
return {
|
|
48
|
+
id: result.id,
|
|
49
|
+
url: result.short_url
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async getSubscription(gatewayId, config) {
|
|
53
|
+
const instance = this.getInstance(config);
|
|
54
|
+
return await instance.subscriptions.fetch(gatewayId);
|
|
55
|
+
}
|
|
56
|
+
async cancelSubscription(gatewayId, cancelAtCycleEnd, config) {
|
|
57
|
+
const instance = this.getInstance(config);
|
|
58
|
+
return await instance.subscriptions.cancel(gatewayId, cancelAtCycleEnd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.RazorpayAdapter = RazorpayAdapter;
|
|
@@ -6,4 +6,4 @@ export declare function sendAutoPostForm(req: Request, res: Response, action: st
|
|
|
6
6
|
export declare function buildProcessingPageHtml(innerHtml: string, loadingSVG?: string, title?: string, headScripts?: string, bodyScripts?: string): string;
|
|
7
7
|
export declare function renderProcessingPage(req: Request, res: Response, innerHtml: string, loadingSVG?: string, headScripts?: string, bodyScripts?: string): Response<any, Record<string, any>>;
|
|
8
8
|
export declare function renderPaytmJsCheckout(req: Request, res: Response, paytmJsToken: any, config: NPConfig): Response<any, Record<string, any>>;
|
|
9
|
-
export declare function renderRazorpayCheckout(req: Request, res: Response, params: Record<string, any>, config: NPConfig, loadingSVG: string): Response<any, Record<string, any>>;
|
|
9
|
+
export declare function renderRazorpayCheckout(req: Request, res: Response, params: Record<string, any>, config: NPConfig, loadingSVG: string, isSubscription?: boolean): Response<any, Record<string, any>>;
|
|
@@ -57,16 +57,14 @@ function renderPaytmJsCheckout(req, res, paytmJsToken, config) {
|
|
|
57
57
|
const html = (0, paytm_1.createPaytmJsCheckoutHtml)(paytmJsToken, config);
|
|
58
58
|
return res.send(html);
|
|
59
59
|
}
|
|
60
|
-
function renderRazorpayCheckout(req, res, params, config, loadingSVG) {
|
|
60
|
+
function renderRazorpayCheckout(req, res, params, config, loadingSVG, isSubscription = false) {
|
|
61
61
|
var _a, _b;
|
|
62
62
|
const options = {
|
|
63
63
|
key: String(config.KEY),
|
|
64
|
-
amount: Number(params['TXN_AMOUNT']) * 100,
|
|
65
64
|
currency: 'INR',
|
|
66
65
|
name: params['PRODUCT_NAME'],
|
|
67
|
-
description: `Order # ${params['ORDER_ID']}`,
|
|
66
|
+
description: isSubscription ? `Subscription` : `Order # ${params['ORDER_ID']}`,
|
|
68
67
|
image: ((_a = config.theme) === null || _a === void 0 ? void 0 : _a.logo) || '',
|
|
69
|
-
order_id: params['ORDER_ID'],
|
|
70
68
|
callback_url: params['CALLBACK_URL'],
|
|
71
69
|
prefill: {
|
|
72
70
|
name: params['NAME'],
|
|
@@ -77,10 +75,17 @@ function renderRazorpayCheckout(req, res, params, config, loadingSVG) {
|
|
|
77
75
|
color: ((_b = config.theme) === null || _b === void 0 ? void 0 : _b.accent) || '#086cfe'
|
|
78
76
|
}
|
|
79
77
|
};
|
|
78
|
+
if (isSubscription) {
|
|
79
|
+
options.subscription_id = params['ORDER_ID'];
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
options.amount = Number(params['TXN_AMOUNT']) * 100;
|
|
83
|
+
options.order_id = params['ORDER_ID'];
|
|
84
|
+
}
|
|
80
85
|
if (wantsJson(req)) {
|
|
81
|
-
return res.json({ provider: 'razorpay', options, failForm: { action: params['CALLBACK_URL'], fields: { razorpay_order_id: params['ORDER_ID'] } }, loadingSVG });
|
|
86
|
+
return res.json({ provider: 'razorpay', options, failForm: { action: params['CALLBACK_URL'], fields: isSubscription ? { razorpay_subscription_id: params['ORDER_ID'] } : { razorpay_order_id: params['ORDER_ID'] } }, loadingSVG });
|
|
82
87
|
}
|
|
83
|
-
const fail = `<div style="display:none"><form method="post" action="${params['CALLBACK_URL']}" id="fail"><input name="razorpay_order_id" value="${params['ORDER_ID']}" hidden="true"/></form></div>`;
|
|
88
|
+
const fail = `<div style="display:none"><form method="post" action="${params['CALLBACK_URL']}" id="fail"><input name="${isSubscription ? 'razorpay_subscription_id' : 'razorpay_order_id'}" value="${params['ORDER_ID']}" hidden="true"/></form></div>`;
|
|
84
89
|
const scriptOptions = `
|
|
85
90
|
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
|
|
86
91
|
<script>
|
|
@@ -50,6 +50,7 @@ const user_controller_1 = require("./user.controller");
|
|
|
50
50
|
const utils_1 = require("../utils/utils");
|
|
51
51
|
const loadingsvg_1 = require("./static/loadingsvg");
|
|
52
52
|
const htmlhelper_1 = require("./htmlhelper");
|
|
53
|
+
const subscription_webhook_1 = require("./subscription.webhook");
|
|
53
54
|
const buildConfig_1 = require("../utils/buildConfig");
|
|
54
55
|
const IDLEN = 14;
|
|
55
56
|
function makeid(length) {
|
|
@@ -62,7 +63,7 @@ function makeid(length) {
|
|
|
62
63
|
}
|
|
63
64
|
class PaymentController {
|
|
64
65
|
constructor(baseConfig, db, callbacks, tableNames) {
|
|
65
|
-
this.tableNames = { USER: 'npusers', TRANSACTION: 'nptransactions' };
|
|
66
|
+
this.tableNames = { USER: 'npusers', TRANSACTION: 'nptransactions', PLAN: 'npplans', SUBSCRIPTION: 'npsubscriptions' };
|
|
66
67
|
this.viewPath = '';
|
|
67
68
|
this.baseConfig = baseConfig;
|
|
68
69
|
this.callbacks = callbacks;
|
|
@@ -515,12 +516,37 @@ class PaymentController {
|
|
|
515
516
|
const orderToFind = req.body.ORDERID || req.body.ORDER_ID || req.body.ORDERId || (req.query && req.query.order_id) || req.body.ORDER_ID;
|
|
516
517
|
const myquery = { orderId: orderToFind };
|
|
517
518
|
let objForUpdate = null;
|
|
519
|
+
let isSubscription = false;
|
|
518
520
|
try {
|
|
519
521
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, myquery).catch(() => null);
|
|
520
522
|
if (!objForUpdate)
|
|
521
523
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, { id: orderToFind }).catch(() => null);
|
|
522
524
|
if (!objForUpdate)
|
|
523
525
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, { ORDERID: orderToFind }).catch(() => null);
|
|
526
|
+
if (!objForUpdate) {
|
|
527
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id: orderToFind }).catch(() => null);
|
|
528
|
+
if (sub) {
|
|
529
|
+
isSubscription = true;
|
|
530
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: sub.planId }).catch(() => null);
|
|
531
|
+
const user = await this.db.getOne(this.tableNames.USER, { id: sub.cusId }).catch(() => null);
|
|
532
|
+
objForUpdate = {
|
|
533
|
+
id: sub.id,
|
|
534
|
+
orderId: sub.id,
|
|
535
|
+
cusId: sub.cusId,
|
|
536
|
+
time: sub.createdAt || Date.now(),
|
|
537
|
+
status: 'INITIATED',
|
|
538
|
+
name: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
539
|
+
email: (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
540
|
+
phone: (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
541
|
+
amount: (plan === null || plan === void 0 ? void 0 : plan.amount) || 0,
|
|
542
|
+
pname: (plan === null || plan === void 0 ? void 0 : plan.name) || 'Subscription',
|
|
543
|
+
extra: '',
|
|
544
|
+
clientId: sub.clientId,
|
|
545
|
+
returnUrl: sub.returnUrl || '',
|
|
546
|
+
webhookUrl: sub.webhookUrl || ''
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
524
550
|
}
|
|
525
551
|
catch {
|
|
526
552
|
objForUpdate = objForUpdate || null;
|
|
@@ -586,7 +612,20 @@ class PaymentController {
|
|
|
586
612
|
objForUpdate.txnId = req.body.TXNID;
|
|
587
613
|
objForUpdate.extra = JSON.stringify(req.body);
|
|
588
614
|
try {
|
|
589
|
-
|
|
615
|
+
if (isSubscription) {
|
|
616
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id: orderToFind }, { status: 'AUTHENTICATED', updatedAt: Date.now() });
|
|
617
|
+
// Also persist a transaction record so status/history APIs find it
|
|
618
|
+
const existingTxn = await this.db.getOne(this.tableNames.TRANSACTION, { orderId: orderToFind }).catch(() => null);
|
|
619
|
+
if (!existingTxn) {
|
|
620
|
+
await this.db.insert(this.tableNames.TRANSACTION, objForUpdate);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
await this.db.update(this.tableNames.TRANSACTION, { orderId: orderToFind }, objForUpdate);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
await this.db.update(this.tableNames.TRANSACTION, myquery, objForUpdate);
|
|
628
|
+
}
|
|
590
629
|
}
|
|
591
630
|
catch {
|
|
592
631
|
if (returnUrl) {
|
|
@@ -655,7 +694,37 @@ class PaymentController {
|
|
|
655
694
|
}
|
|
656
695
|
let result = false;
|
|
657
696
|
let isCancelled = false;
|
|
658
|
-
|
|
697
|
+
let objForUpdate = await this.getOrder(req);
|
|
698
|
+
let errorMessage = null;
|
|
699
|
+
let isSubscriptionCallback = false;
|
|
700
|
+
// Razorpay Subscription Callback Handling
|
|
701
|
+
if (!objForUpdate && req.body.razorpay_subscription_id) {
|
|
702
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { gateway_subscription_id: req.body.razorpay_subscription_id });
|
|
703
|
+
if (sub) {
|
|
704
|
+
isSubscriptionCallback = true;
|
|
705
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: sub.planId }).catch(() => null);
|
|
706
|
+
const user = await this.db.getOne(this.tableNames.USER, { id: sub.cusId }).catch(() => null);
|
|
707
|
+
// Create a virtual transaction object for the callback processor
|
|
708
|
+
objForUpdate = {
|
|
709
|
+
id: sub.id,
|
|
710
|
+
orderId: sub.id,
|
|
711
|
+
cusId: sub.cusId,
|
|
712
|
+
time: Date.now(),
|
|
713
|
+
status: 'INITIATED',
|
|
714
|
+
name: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
715
|
+
email: (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
716
|
+
phone: (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
717
|
+
amount: (plan === null || plan === void 0 ? void 0 : plan.amount) || 0,
|
|
718
|
+
pname: (plan === null || plan === void 0 ? void 0 : plan.name) || 'Subscription Authentication',
|
|
719
|
+
extra: '',
|
|
720
|
+
clientId: sub.clientId,
|
|
721
|
+
returnUrl: sub.returnUrl || '',
|
|
722
|
+
webhookUrl: sub.webhookUrl || '',
|
|
723
|
+
isSubscription: true,
|
|
724
|
+
subscriptionId: sub.id
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
659
728
|
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, objForUpdate);
|
|
660
729
|
const payuInstance = this.getProviderInstance('PayU', (0, buildConfig_1.withClientConfigOverrides)(config, req, objForUpdate));
|
|
661
730
|
const openMoneyInstance = this.getProviderInstance('OpenMoney', (0, buildConfig_1.withClientConfigOverrides)(config, req, objForUpdate));
|
|
@@ -678,28 +747,52 @@ class PaymentController {
|
|
|
678
747
|
}
|
|
679
748
|
}
|
|
680
749
|
else if (config.razor_url) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
liveResonse = await this.getProviderInstance('Razorpay', config).orders.fetch(orderid).catch(() => null);
|
|
685
|
-
req.body.extras = liveResonse;
|
|
686
|
-
}
|
|
687
|
-
if (req.body.razorpay_payment_id) {
|
|
688
|
-
result = checksum_1.default.checkRazorSignature(req.body.razorpay_order_id, req.body.razorpay_payment_id, config.SECRET, req.body.razorpay_signature);
|
|
750
|
+
if (isSubscriptionCallback) {
|
|
751
|
+
// Subscription verification: Razorpay expects payment_id + "|" + subscription_id
|
|
752
|
+
result = checksum_1.default.checkRazorSignature(req.body.razorpay_payment_id, req.body.razorpay_subscription_id, config.SECRET, req.body.razorpay_signature);
|
|
689
753
|
if (result) {
|
|
690
754
|
req.body.STATUS = 'TXN_SUCCESS';
|
|
691
|
-
req.body.ORDERID =
|
|
755
|
+
req.body.ORDERID = objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id;
|
|
692
756
|
req.body.TXNID = req.body.razorpay_payment_id;
|
|
757
|
+
// Update local subscription status
|
|
758
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id: objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id }, { status: 'AUTHENTICATED', updatedAt: Date.now() });
|
|
759
|
+
}
|
|
760
|
+
else if (objForUpdate) {
|
|
761
|
+
req.body.ORDERID = objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id;
|
|
762
|
+
req.body.STATUS = 'TXN_FAILURE';
|
|
763
|
+
errorMessage = 'Subscription signature verification failed';
|
|
693
764
|
}
|
|
694
765
|
}
|
|
695
766
|
else {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
767
|
+
// Standard Order verification
|
|
768
|
+
let orderid = req.body.razorpay_order_id || req.query.ORDERID || req.query.order_id;
|
|
769
|
+
let liveResonse = null;
|
|
770
|
+
if (orderid) {
|
|
771
|
+
liveResonse = await this.getProviderInstance('Razorpay', config).orders.fetch(orderid).catch(() => null);
|
|
772
|
+
req.body.extras = liveResonse;
|
|
773
|
+
}
|
|
774
|
+
if (req.body.razorpay_payment_id) {
|
|
775
|
+
result = checksum_1.default.checkRazorSignature(req.body.razorpay_order_id, req.body.razorpay_payment_id, config.SECRET, req.body.razorpay_signature);
|
|
776
|
+
if (result) {
|
|
777
|
+
req.body.STATUS = 'TXN_SUCCESS';
|
|
778
|
+
req.body.ORDERID = req.body.razorpay_order_id;
|
|
779
|
+
req.body.TXNID = req.body.razorpay_payment_id;
|
|
780
|
+
}
|
|
781
|
+
else if (objForUpdate) {
|
|
782
|
+
req.body.ORDERID = objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id;
|
|
783
|
+
req.body.STATUS = 'TXN_FAILURE';
|
|
784
|
+
errorMessage = 'Subscription signature verification failed';
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
if (req.body.error && req.body.error.metadata && JSON.parse(req.body.error.metadata)) {
|
|
789
|
+
const orderId = JSON.parse(req.body.error.metadata).order_id;
|
|
790
|
+
req.body.razorpay_order_id = orderId;
|
|
791
|
+
}
|
|
792
|
+
req.body.STATUS = (liveResonse === null || liveResonse === void 0 ? void 0 : liveResonse.attempts) ? 'TXN_FAILURE' : 'CANCELLED';
|
|
793
|
+
req.body.ORDERID = req.body.razorpay_order_id || req.query.order_id;
|
|
794
|
+
isCancelled = true;
|
|
699
795
|
}
|
|
700
|
-
req.body.STATUS = (liveResonse === null || liveResonse === void 0 ? void 0 : liveResonse.attempts) ? 'TXN_FAILURE' : 'CANCELLED';
|
|
701
|
-
req.body.ORDERID = req.body.razorpay_order_id || req.query.order_id;
|
|
702
|
-
isCancelled = true;
|
|
703
796
|
}
|
|
704
797
|
}
|
|
705
798
|
else if (config.payu_url) {
|
|
@@ -719,13 +812,8 @@ class PaymentController {
|
|
|
719
812
|
req.body.ORDERID = openRest.ORDERID || req.query.order_id;
|
|
720
813
|
req.body.extras = openRest.data;
|
|
721
814
|
}
|
|
722
|
-
console.log("NodePayTMPG::Transaction => ", req.body.ORDERID, req.body.STATUS);
|
|
723
|
-
|
|
724
|
-
await this.updateTransaction(req, res);
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
res.send({ message: "Something went wrong ! Please try again later .", ORDERID: req.body.ORDERID, TXNID: req.body.TXNID });
|
|
728
|
-
}
|
|
815
|
+
console.log("NodePayTMPG::Transaction => ", req.body.ORDERID, req.body.STATUS, errorMessage || '', 'isCancelled:', isCancelled, 'isSubscriptionCallback:', isSubscriptionCallback);
|
|
816
|
+
await this.updateTransaction(req, res);
|
|
729
817
|
}
|
|
730
818
|
getServiceUsed(req, baseConfig) {
|
|
731
819
|
const config = (0, buildConfig_1.withClientConfigOverrides)(baseConfig, req);
|
|
@@ -763,8 +851,21 @@ class PaymentController {
|
|
|
763
851
|
return;
|
|
764
852
|
}
|
|
765
853
|
if (serviceUsed === 'Razorpay') {
|
|
766
|
-
const events = [
|
|
854
|
+
const events = [
|
|
855
|
+
"payment.captured", "payment.pending", "payment.failed",
|
|
856
|
+
"subscription.authenticated", "subscription.paused", "subscription.resumed",
|
|
857
|
+
"subscription.activated", "subscription.pending", "subscription.halted",
|
|
858
|
+
"subscription.charged", "subscription.cancelled", "subscription.completed",
|
|
859
|
+
"subscription.updated"
|
|
860
|
+
];
|
|
767
861
|
if (req.body.event && events.indexOf(req.body.event) > -1) {
|
|
862
|
+
const event = req.body.event;
|
|
863
|
+
// Handle Subscription Events
|
|
864
|
+
if (event.startsWith("subscription.")) {
|
|
865
|
+
await (0, subscription_webhook_1.handleSubscriptionWebhook)(req, res, this.db, this.baseConfig, this.tableNames, makeid);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Handle One-time Payment Events
|
|
768
869
|
if (req.body.payload &&
|
|
769
870
|
req.body.payload.payment &&
|
|
770
871
|
req.body.payload.payment.entity) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MultiDbORM } from 'multi-db-orm';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import { NPConfig, NPTableNames } from '../models';
|
|
4
|
+
export declare class SubscriptionController {
|
|
5
|
+
private baseConfig;
|
|
6
|
+
private db;
|
|
7
|
+
private tableNames;
|
|
8
|
+
private userController;
|
|
9
|
+
constructor(baseConfig: NPConfig, db: MultiDbORM, tableNames?: NPTableNames);
|
|
10
|
+
private configure;
|
|
11
|
+
private getProvider;
|
|
12
|
+
createPlan(req: Request, res: Response): Promise<void>;
|
|
13
|
+
getPlans(req: Request, res: Response): Promise<void>;
|
|
14
|
+
getPlan(req: Request, res: Response): Promise<void>;
|
|
15
|
+
updatePlan(req: Request, res: Response): Promise<void>;
|
|
16
|
+
deletePlan(req: Request, res: Response): Promise<void>;
|
|
17
|
+
initSubscription(req: Request, res: Response): Promise<void>;
|
|
18
|
+
checkoutSubscription(req: Request, res: Response): Promise<void>;
|
|
19
|
+
getSubscription(req: Request, res: Response): Promise<void>;
|
|
20
|
+
cancelSubscription(req: Request, res: Response): Promise<void>;
|
|
21
|
+
getSubscriptionPayments(req: Request, res: Response): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SubscriptionController = void 0;
|
|
4
|
+
const buildConfig_1 = require("../utils/buildConfig");
|
|
5
|
+
const razorpay_1 = require("./adapters/razorpay");
|
|
6
|
+
const user_controller_1 = require("./user.controller");
|
|
7
|
+
const utils_1 = require("../utils/utils");
|
|
8
|
+
const htmlhelper_1 = require("./htmlhelper");
|
|
9
|
+
const loadingsvg_1 = require("./static/loadingsvg");
|
|
10
|
+
class SubscriptionController {
|
|
11
|
+
constructor(baseConfig, db, tableNames) {
|
|
12
|
+
this.tableNames = { USER: 'npusers', TRANSACTION: 'nptransactions', PLAN: 'npplans', SUBSCRIPTION: 'npsubscriptions' };
|
|
13
|
+
this.baseConfig = baseConfig;
|
|
14
|
+
this.db = db;
|
|
15
|
+
if (tableNames) {
|
|
16
|
+
this.tableNames = { ...this.tableNames, ...tableNames };
|
|
17
|
+
}
|
|
18
|
+
this.userController = new user_controller_1.NPUserController(this.db, this.tableNames.USER);
|
|
19
|
+
this.configure();
|
|
20
|
+
}
|
|
21
|
+
configure() {
|
|
22
|
+
const planSample = {
|
|
23
|
+
id: 'plan_sample',
|
|
24
|
+
name: 'Sample Plan',
|
|
25
|
+
description: 'Sample Plan',
|
|
26
|
+
amount: 100,
|
|
27
|
+
currency: 'INR',
|
|
28
|
+
period: 'monthly',
|
|
29
|
+
interval: 1,
|
|
30
|
+
clientId: 'client_1',
|
|
31
|
+
gateway_plan_id: 'gw_plan_sample'
|
|
32
|
+
};
|
|
33
|
+
const subSample = {
|
|
34
|
+
id: 'sub_sample',
|
|
35
|
+
planId: 'plan_sample',
|
|
36
|
+
cusId: 'user_sample',
|
|
37
|
+
status: 'CREATED',
|
|
38
|
+
clientId: 'client_1',
|
|
39
|
+
gateway_subscription_id: 'gw_sub_sample'
|
|
40
|
+
};
|
|
41
|
+
this.db.create(this.tableNames.PLAN, planSample).catch(() => { });
|
|
42
|
+
this.db.create(this.tableNames.SUBSCRIPTION, subSample).catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
getProvider(config) {
|
|
45
|
+
if (config.razor_url) {
|
|
46
|
+
return new razorpay_1.RazorpayAdapter();
|
|
47
|
+
}
|
|
48
|
+
// Future: add PayU adapter here
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// --- PLAN MANAGEMENT ---
|
|
52
|
+
async createPlan(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
55
|
+
const provider = this.getProvider(config);
|
|
56
|
+
if (!provider) {
|
|
57
|
+
res.status(400).send({ message: 'No supported subscription provider configured.' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const { id, name, description, amount, currency, period, interval, trial_days, clientId } = req.body;
|
|
61
|
+
if (!id || !name || !amount || !period || !interval) {
|
|
62
|
+
res.status(400).send({ message: 'Missing required fields: id, name, amount, period, interval' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Check if plan already exists locally
|
|
66
|
+
const existingPlan = await this.db.getOne(this.tableNames.PLAN, { id });
|
|
67
|
+
if (existingPlan) {
|
|
68
|
+
res.status(409).send({ message: 'Plan ID already exists locally.' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const planData = {
|
|
72
|
+
id, name, description, amount: parseFloat(amount),
|
|
73
|
+
currency: currency || 'INR', period, interval: parseInt(interval, 10),
|
|
74
|
+
trial_days: trial_days ? parseInt(trial_days, 10) : 0,
|
|
75
|
+
clientId: clientId || req.query.client_id || '',
|
|
76
|
+
createdAt: Date.now(), updatedAt: Date.now(), is_deleted: false
|
|
77
|
+
};
|
|
78
|
+
// Register plan with Gateway
|
|
79
|
+
try {
|
|
80
|
+
const gatewayPlanId = await provider.createPlan(planData, config);
|
|
81
|
+
planData.gateway_plan_id = gatewayPlanId;
|
|
82
|
+
}
|
|
83
|
+
catch (gwErr) {
|
|
84
|
+
console.error("Gateway Create Plan Error:", gwErr);
|
|
85
|
+
res.status(500).send({ message: 'Failed to create plan on Gateway', error: (gwErr === null || gwErr === void 0 ? void 0 : gwErr.message) || gwErr });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Save locally
|
|
89
|
+
await this.db.insert(this.tableNames.PLAN, planData);
|
|
90
|
+
res.status(201).send(planData);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error("Create Plan Error:", err);
|
|
94
|
+
res.status(500).send({ message: 'Internal Server Error', error: err === null || err === void 0 ? void 0 : err.message });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async getPlans(req, res) {
|
|
98
|
+
try {
|
|
99
|
+
const clientId = req.query.clientId || req.query.client_id || req.headers['x-client-id'] || '';
|
|
100
|
+
const query = { is_deleted: false };
|
|
101
|
+
if (clientId) {
|
|
102
|
+
query.clientId = clientId;
|
|
103
|
+
}
|
|
104
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
105
|
+
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
|
106
|
+
const plans = await this.db.get(this.tableNames.PLAN, query, {
|
|
107
|
+
sort: [{ field: 'createdAt', order: 'desc' }],
|
|
108
|
+
limit: limit, offset: offset
|
|
109
|
+
});
|
|
110
|
+
res.send({ limit, offset, count: plans.length, plans });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
res.status(500).send({ message: 'Error fetching plans', error: err === null || err === void 0 ? void 0 : err.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async getPlan(req, res) {
|
|
117
|
+
try {
|
|
118
|
+
const id = req.params.id;
|
|
119
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id, is_deleted: false });
|
|
120
|
+
if (!plan) {
|
|
121
|
+
res.status(404).send({ message: 'Plan not found' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
res.send(plan);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
res.status(500).send({ message: 'Error fetching plan', error: err === null || err === void 0 ? void 0 : err.message });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async updatePlan(req, res) {
|
|
131
|
+
try {
|
|
132
|
+
const id = req.params.id;
|
|
133
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id, is_deleted: false });
|
|
134
|
+
if (!plan) {
|
|
135
|
+
res.status(404).send({ message: 'Plan not found' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const { name, description, amount, interval, period, currency, trial_days } = req.body;
|
|
139
|
+
// Check if Gateway immutable fields are changing
|
|
140
|
+
let needsNewGatewayPlan = false;
|
|
141
|
+
if ((amount !== undefined && parseFloat(amount) !== plan.amount) ||
|
|
142
|
+
(interval !== undefined && parseInt(interval, 10) !== plan.interval) ||
|
|
143
|
+
(period !== undefined && period !== plan.period) ||
|
|
144
|
+
(currency !== undefined && currency !== plan.currency) ||
|
|
145
|
+
(trial_days !== undefined && parseInt(trial_days, 10) !== plan.trial_days)) {
|
|
146
|
+
needsNewGatewayPlan = true;
|
|
147
|
+
}
|
|
148
|
+
const updatedPlan = { ...plan, updatedAt: Date.now() };
|
|
149
|
+
if (name !== undefined)
|
|
150
|
+
updatedPlan.name = name;
|
|
151
|
+
if (description !== undefined)
|
|
152
|
+
updatedPlan.description = description;
|
|
153
|
+
if (amount !== undefined)
|
|
154
|
+
updatedPlan.amount = parseFloat(amount);
|
|
155
|
+
if (interval !== undefined)
|
|
156
|
+
updatedPlan.interval = parseInt(interval, 10);
|
|
157
|
+
if (period !== undefined)
|
|
158
|
+
updatedPlan.period = period;
|
|
159
|
+
if (currency !== undefined)
|
|
160
|
+
updatedPlan.currency = currency;
|
|
161
|
+
if (trial_days !== undefined)
|
|
162
|
+
updatedPlan.trial_days = parseInt(trial_days, 10);
|
|
163
|
+
if (needsNewGatewayPlan) {
|
|
164
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
165
|
+
const provider = this.getProvider(config);
|
|
166
|
+
if (provider) {
|
|
167
|
+
try {
|
|
168
|
+
const newGatewayId = await provider.createPlan(updatedPlan, config);
|
|
169
|
+
updatedPlan.gateway_plan_id = newGatewayId;
|
|
170
|
+
}
|
|
171
|
+
catch (gwErr) {
|
|
172
|
+
res.status(500).send({ message: 'Failed to create updated plan on Gateway', error: gwErr === null || gwErr === void 0 ? void 0 : gwErr.message });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await this.db.update(this.tableNames.PLAN, { id }, updatedPlan);
|
|
178
|
+
res.send(updatedPlan);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
res.status(500).send({ message: 'Error updating plan', error: err === null || err === void 0 ? void 0 : err.message });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async deletePlan(req, res) {
|
|
185
|
+
try {
|
|
186
|
+
const id = req.params.id;
|
|
187
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id });
|
|
188
|
+
if (!plan) {
|
|
189
|
+
res.status(404).send({ message: 'Plan not found' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Soft delete
|
|
193
|
+
await this.db.update(this.tableNames.PLAN, { id }, { ...plan, is_deleted: true, updatedAt: Date.now() });
|
|
194
|
+
res.send({ message: 'Plan deleted successfully', id });
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
res.status(500).send({ message: 'Error deleting plan', error: err === null || err === void 0 ? void 0 : err.message });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// --- SUBSCRIPTION MANAGEMENT ---
|
|
201
|
+
async initSubscription(req, res) {
|
|
202
|
+
var _a;
|
|
203
|
+
try {
|
|
204
|
+
const { planId, returnUrl, webhookUrl, NAME, EMAIL, MOBILE_NO, CLIENT_ID } = req.body;
|
|
205
|
+
if (!planId || !NAME || !EMAIL) {
|
|
206
|
+
res.status(400).send({ message: 'Missing required fields: planId, NAME, EMAIL' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: planId, is_deleted: false });
|
|
210
|
+
if (!plan || !plan.gateway_plan_id) {
|
|
211
|
+
res.status(404).send({ message: 'Active plan not found or not synced with gateway.' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
215
|
+
const provider = this.getProvider(config);
|
|
216
|
+
if (!provider) {
|
|
217
|
+
res.status(400).send({ message: 'No supported subscription provider configured.' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Create/Get User
|
|
221
|
+
const user = await this.userController.create({ name: NAME, email: EMAIL, phone: MOBILE_NO });
|
|
222
|
+
const subId = 'sub_' + utils_1.Utils.makeid(14);
|
|
223
|
+
const subData = {
|
|
224
|
+
id: subId,
|
|
225
|
+
planId: plan.id,
|
|
226
|
+
cusId: user.id,
|
|
227
|
+
status: 'CREATED',
|
|
228
|
+
clientId: CLIENT_ID || req.query.client_id || '',
|
|
229
|
+
returnUrl: returnUrl || '',
|
|
230
|
+
webhookUrl: webhookUrl || '',
|
|
231
|
+
createdAt: Date.now(),
|
|
232
|
+
updatedAt: Date.now()
|
|
233
|
+
};
|
|
234
|
+
// Call Gateway
|
|
235
|
+
try {
|
|
236
|
+
const { id: gateway_sub_id, url: short_url } = await provider.createSubscription(subData, plan, config);
|
|
237
|
+
subData.gateway_subscription_id = gateway_sub_id;
|
|
238
|
+
subData.short_url = short_url;
|
|
239
|
+
}
|
|
240
|
+
catch (gwErr) {
|
|
241
|
+
console.error("Gateway Sub Error:", gwErr);
|
|
242
|
+
res.status(500).send({ message: 'Failed to initialize subscription on gateway', error: gwErr === null || gwErr === void 0 ? void 0 : gwErr.message });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await this.db.insert(this.tableNames.SUBSCRIPTION, subData);
|
|
246
|
+
const responseData = {
|
|
247
|
+
...subData,
|
|
248
|
+
orderId: subData.id,
|
|
249
|
+
payurl: config.host_url + '/' + config.path_prefix + '/sub/checkout/' + subData.id,
|
|
250
|
+
pname: plan.name,
|
|
251
|
+
amount: plan.amount,
|
|
252
|
+
name: user.name,
|
|
253
|
+
email: user.email,
|
|
254
|
+
phone: user.phone
|
|
255
|
+
};
|
|
256
|
+
if (((_a = req.headers.accept) === null || _a === void 0 ? void 0 : _a.includes('application/json')) || req.path.includes('/createTxn')) {
|
|
257
|
+
res.status(201).send(responseData);
|
|
258
|
+
}
|
|
259
|
+
else if (subData.short_url) {
|
|
260
|
+
res.redirect(config.host_url + '/' + config.path_prefix + '/sub/checkout/' + subData.id);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
res.status(201).send(responseData); // fallback
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.error("Init Sub Error:", err);
|
|
268
|
+
res.status(500).send({ message: 'Internal server error', error: err === null || err === void 0 ? void 0 : err.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async checkoutSubscription(req, res) {
|
|
272
|
+
try {
|
|
273
|
+
const id = req.params.id;
|
|
274
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
275
|
+
if (!sub || !sub.gateway_subscription_id) {
|
|
276
|
+
res.status(404).send({ message: 'Subscription not found or not ready.' });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: sub.planId });
|
|
280
|
+
const user = await this.db.getOne(this.tableNames.USER, { id: sub.cusId });
|
|
281
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, { clientId: sub.clientId });
|
|
282
|
+
const params = {
|
|
283
|
+
ORDER_ID: sub.gateway_subscription_id,
|
|
284
|
+
CALLBACK_URL: config.host_url + '/' + config.path_prefix + '/callback',
|
|
285
|
+
NAME: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
286
|
+
EMAIL: (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
287
|
+
MOBILE_NO: (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
288
|
+
PRODUCT_NAME: (plan === null || plan === void 0 ? void 0 : plan.name) || ''
|
|
289
|
+
};
|
|
290
|
+
(0, htmlhelper_1.renderRazorpayCheckout)(req, res, params, config, loadingsvg_1.LoadingSVG, true);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
res.status(500).send({ message: 'Error rendering checkout', error: err === null || err === void 0 ? void 0 : err.message });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async getSubscription(req, res) {
|
|
297
|
+
try {
|
|
298
|
+
const id = req.params.id;
|
|
299
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
300
|
+
if (!sub) {
|
|
301
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Optionally sync from provider
|
|
305
|
+
if (req.query.sync && sub.gateway_subscription_id) {
|
|
306
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
307
|
+
const provider = this.getProvider(config);
|
|
308
|
+
if (provider) {
|
|
309
|
+
try {
|
|
310
|
+
const gwData = await provider.getSubscription(sub.gateway_subscription_id, config);
|
|
311
|
+
let newStatus = sub.status;
|
|
312
|
+
if (gwData.status === 'active')
|
|
313
|
+
newStatus = 'ACTIVE';
|
|
314
|
+
else if (gwData.status === 'authenticated')
|
|
315
|
+
newStatus = 'AUTHENTICATED';
|
|
316
|
+
else if (gwData.status === 'cancelled')
|
|
317
|
+
newStatus = 'CANCELLED';
|
|
318
|
+
else if (gwData.status === 'completed')
|
|
319
|
+
newStatus = 'COMPLETED';
|
|
320
|
+
else if (gwData.status === 'expired')
|
|
321
|
+
newStatus = 'EXPIRED';
|
|
322
|
+
else if (gwData.status === 'pending' || gwData.status === 'halted')
|
|
323
|
+
newStatus = 'HALTED';
|
|
324
|
+
if (newStatus !== sub.status) {
|
|
325
|
+
sub.status = newStatus;
|
|
326
|
+
sub.updatedAt = Date.now();
|
|
327
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id }, sub);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (gwErr) {
|
|
331
|
+
console.error('Failed to sync sub status:', gwErr);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
res.send(sub);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
res.status(500).send({ message: 'Error fetching subscription', error: err === null || err === void 0 ? void 0 : err.message });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async cancelSubscription(req, res) {
|
|
342
|
+
try {
|
|
343
|
+
const id = req.params.id;
|
|
344
|
+
const cancelAtCycleEnd = req.body.cancel_at_cycle_end === true || req.body.cancel_at_cycle_end === 'true';
|
|
345
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
346
|
+
if (!sub) {
|
|
347
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (sub.status === 'CANCELLED' || sub.status === 'EXPIRED' || sub.status === 'COMPLETED') {
|
|
351
|
+
res.status(400).send({ message: `Cannot cancel subscription in ${sub.status} state` });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
355
|
+
const provider = this.getProvider(config);
|
|
356
|
+
if (provider && sub.gateway_subscription_id) {
|
|
357
|
+
try {
|
|
358
|
+
await provider.cancelSubscription(sub.gateway_subscription_id, cancelAtCycleEnd, config);
|
|
359
|
+
if (!cancelAtCycleEnd) {
|
|
360
|
+
sub.status = 'CANCELLED';
|
|
361
|
+
}
|
|
362
|
+
sub.updatedAt = Date.now();
|
|
363
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id }, sub);
|
|
364
|
+
res.send({ message: 'Cancellation processed successfully', status: sub.status });
|
|
365
|
+
}
|
|
366
|
+
catch (gwErr) {
|
|
367
|
+
res.status(500).send({ message: 'Failed to cancel on gateway', error: (gwErr === null || gwErr === void 0 ? void 0 : gwErr.message) || gwErr });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
res.status(400).send({ message: 'No provider configured or missing gateway subscription ID' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
res.status(500).send({ message: 'Error cancelling subscription', error: err === null || err === void 0 ? void 0 : err.message });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async getSubscriptionPayments(req, res) {
|
|
379
|
+
try {
|
|
380
|
+
const id = req.params.id;
|
|
381
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
382
|
+
if (!sub) {
|
|
383
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
387
|
+
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
|
388
|
+
// Fetch transactions linked to this subscription
|
|
389
|
+
const payments = await this.db.get(this.tableNames.TRANSACTION, { subscriptionId: id }, {
|
|
390
|
+
sort: [{ field: 'time', order: 'desc' }],
|
|
391
|
+
limit: limit, offset: offset
|
|
392
|
+
});
|
|
393
|
+
res.send({ limit, offset, count: payments.length, payments });
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
res.status(500).send({ message: 'Error fetching payments', error: err === null || err === void 0 ? void 0 : err.message });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
exports.SubscriptionController = SubscriptionController;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { MultiDbORM } from 'multi-db-orm';
|
|
3
|
+
import { NPConfig, NPTableNames } from '../models';
|
|
4
|
+
export declare function handleSubscriptionWebhook(req: Request, res: Response, db: MultiDbORM, baseConfig: NPConfig, tableNames: NPTableNames, makeid: (length: number) => string): Promise<void>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleSubscriptionWebhook = handleSubscriptionWebhook;
|
|
7
|
+
const razorpay_1 = __importDefault(require("razorpay"));
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const buildConfig_1 = require("../utils/buildConfig");
|
|
10
|
+
async function handleSubscriptionWebhook(req, res, db, baseConfig, tableNames, makeid) {
|
|
11
|
+
var _a;
|
|
12
|
+
const event = req.body.event;
|
|
13
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(baseConfig, req);
|
|
14
|
+
if (req.body.payload && req.body.payload.subscription && req.body.payload.subscription.entity) {
|
|
15
|
+
const subEntity = req.body.payload.subscription.entity;
|
|
16
|
+
const paymentEntity = (_a = req.body.payload.payment) === null || _a === void 0 ? void 0 : _a.entity;
|
|
17
|
+
const gateway_subscription_id = subEntity.id;
|
|
18
|
+
const reqBody = req.rawBody;
|
|
19
|
+
const signature = req.headers["x-razorpay-signature"];
|
|
20
|
+
if (signature === undefined) {
|
|
21
|
+
res.status(200).send({ message: "Missing Razorpay signature" });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let signatureValid;
|
|
25
|
+
try {
|
|
26
|
+
signatureValid = razorpay_1.default.validateWebhookSignature(reqBody, signature, config.SECRET);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
signatureValid = false;
|
|
30
|
+
}
|
|
31
|
+
if (!signatureValid) {
|
|
32
|
+
res.status(200).send({ message: "Invalid Rzpay signature" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Find the local subscription
|
|
36
|
+
const sub = await db.getOne(tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { gateway_subscription_id });
|
|
37
|
+
if (!sub) {
|
|
38
|
+
console.log("Subscription not found for webhook:", gateway_subscription_id);
|
|
39
|
+
res.status(200).send({ message: "Subscription not found locally" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const clientConf = (0, buildConfig_1.withClientConfigOverrides)(baseConfig, req, { clientId: sub.clientId });
|
|
43
|
+
let statusChanged = false;
|
|
44
|
+
// Map Razorpay events to local subscription status
|
|
45
|
+
switch (event) {
|
|
46
|
+
case "subscription.authenticated":
|
|
47
|
+
sub.status = 'AUTHENTICATED';
|
|
48
|
+
statusChanged = true;
|
|
49
|
+
// Trigger Setup Success Webhook
|
|
50
|
+
const planAuth = await db.getOne(tableNames.PLAN, { id: sub.planId }).catch(() => null);
|
|
51
|
+
const userAuth = await db.getOne(tableNames.USER, { id: sub.cusId }).catch(() => null);
|
|
52
|
+
const authTxn = {
|
|
53
|
+
id: sub.id,
|
|
54
|
+
orderId: sub.id,
|
|
55
|
+
cusId: sub.cusId,
|
|
56
|
+
time: Date.now(),
|
|
57
|
+
status: 'TXN_SUCCESS',
|
|
58
|
+
name: (userAuth === null || userAuth === void 0 ? void 0 : userAuth.name) || '',
|
|
59
|
+
email: (userAuth === null || userAuth === void 0 ? void 0 : userAuth.email) || '',
|
|
60
|
+
phone: (userAuth === null || userAuth === void 0 ? void 0 : userAuth.phone) || '',
|
|
61
|
+
amount: (planAuth === null || planAuth === void 0 ? void 0 : planAuth.amount) || 0,
|
|
62
|
+
pname: (planAuth === null || planAuth === void 0 ? void 0 : planAuth.name) || 'Subscription Authentication',
|
|
63
|
+
extra: JSON.stringify(subEntity),
|
|
64
|
+
txnId: (paymentEntity === null || paymentEntity === void 0 ? void 0 : paymentEntity.id) || '',
|
|
65
|
+
clientId: sub.clientId,
|
|
66
|
+
returnUrl: sub.returnUrl || '',
|
|
67
|
+
webhookUrl: sub.webhookUrl || '',
|
|
68
|
+
isSubscription: true,
|
|
69
|
+
subscriptionId: sub.id
|
|
70
|
+
};
|
|
71
|
+
// Persist if doesn't exist
|
|
72
|
+
const existingAuth = await db.getOne(tableNames.TRANSACTION, { orderId: sub.id }).catch(() => null);
|
|
73
|
+
if (!existingAuth) {
|
|
74
|
+
await db.insert(tableNames.TRANSACTION, authTxn);
|
|
75
|
+
}
|
|
76
|
+
if (sub.webhookUrl) {
|
|
77
|
+
try {
|
|
78
|
+
await axios_1.default.post(sub.webhookUrl, authTxn);
|
|
79
|
+
}
|
|
80
|
+
catch (e) { }
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case "subscription.activated":
|
|
84
|
+
case "subscription.resumed":
|
|
85
|
+
case "subscription.updated": // An update might make it active again or just change metadata
|
|
86
|
+
if (subEntity.status === 'active') {
|
|
87
|
+
sub.status = 'ACTIVE';
|
|
88
|
+
statusChanged = true;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "subscription.paused":
|
|
92
|
+
sub.status = 'PAUSED';
|
|
93
|
+
statusChanged = true;
|
|
94
|
+
break;
|
|
95
|
+
case "subscription.pending":
|
|
96
|
+
sub.status = 'PENDING';
|
|
97
|
+
statusChanged = true;
|
|
98
|
+
break;
|
|
99
|
+
case "subscription.halted":
|
|
100
|
+
sub.status = 'HALTED';
|
|
101
|
+
statusChanged = true;
|
|
102
|
+
break;
|
|
103
|
+
case "subscription.cancelled":
|
|
104
|
+
sub.status = 'CANCELLED';
|
|
105
|
+
statusChanged = true;
|
|
106
|
+
break;
|
|
107
|
+
case "subscription.completed":
|
|
108
|
+
sub.status = 'COMPLETED';
|
|
109
|
+
statusChanged = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (statusChanged) {
|
|
113
|
+
sub.updatedAt = Date.now();
|
|
114
|
+
await db.update(tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
115
|
+
}
|
|
116
|
+
// Trigger client payment webhook ONLY on actual charges or definitive failures
|
|
117
|
+
if (event === "subscription.charged" && paymentEntity) {
|
|
118
|
+
sub.status = 'ACTIVE';
|
|
119
|
+
await db.update(tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
120
|
+
const plan = await db.getOne(tableNames.PLAN, { id: sub.planId }).catch(() => null);
|
|
121
|
+
const user = await db.getOne(tableNames.USER, { id: sub.cusId }).catch(() => null);
|
|
122
|
+
// Create a new transaction record for this specific charge
|
|
123
|
+
const txnId = 'txn_' + makeid(10);
|
|
124
|
+
const newTxn = {
|
|
125
|
+
id: txnId,
|
|
126
|
+
orderId: txnId, // Use txnId as orderId for recurring payments since there is no explicit user-created order
|
|
127
|
+
cusId: sub.cusId,
|
|
128
|
+
time: Date.now(),
|
|
129
|
+
status: 'TXN_SUCCESS',
|
|
130
|
+
name: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
131
|
+
email: paymentEntity.email || (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
132
|
+
phone: paymentEntity.contact || (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
133
|
+
amount: paymentEntity.amount / 100,
|
|
134
|
+
pname: (plan === null || plan === void 0 ? void 0 : plan.name) || 'Subscription Charge',
|
|
135
|
+
extra: JSON.stringify(paymentEntity),
|
|
136
|
+
txnId: paymentEntity.id,
|
|
137
|
+
clientId: sub.clientId,
|
|
138
|
+
returnUrl: sub.returnUrl,
|
|
139
|
+
webhookUrl: sub.webhookUrl,
|
|
140
|
+
isSubscription: true,
|
|
141
|
+
subscriptionId: sub.id
|
|
142
|
+
};
|
|
143
|
+
await db.insert(tableNames.TRANSACTION, newTxn);
|
|
144
|
+
// Trigger client webhook
|
|
145
|
+
if (sub.webhookUrl) {
|
|
146
|
+
try {
|
|
147
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
148
|
+
console.log("Sent subscription webhook to ", sub.webhookUrl, 'txnId:', paymentEntity.id);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
console.log("Error sending subscription webhook to ", sub.webhookUrl, (e === null || e === void 0 ? void 0 : e.message) || e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (event === "subscription.halted") {
|
|
156
|
+
const plan = await db.getOne(tableNames.PLAN, { id: sub.planId }).catch(() => null);
|
|
157
|
+
const user = await db.getOne(tableNames.USER, { id: sub.cusId }).catch(() => null);
|
|
158
|
+
// Optional: Inform client of a failed recurring payment that led to a halt
|
|
159
|
+
const txnId = 'txn_' + makeid(10);
|
|
160
|
+
const newTxn = {
|
|
161
|
+
id: txnId,
|
|
162
|
+
orderId: txnId,
|
|
163
|
+
cusId: sub.cusId,
|
|
164
|
+
time: Date.now(),
|
|
165
|
+
status: 'TXN_FAILURE',
|
|
166
|
+
name: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
167
|
+
email: (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
168
|
+
phone: (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
169
|
+
amount: (plan === null || plan === void 0 ? void 0 : plan.amount) || 0,
|
|
170
|
+
pname: (plan === null || plan === void 0 ? void 0 : plan.name) ? `${plan.name} (Halted)` : 'Subscription Halted',
|
|
171
|
+
extra: JSON.stringify(subEntity),
|
|
172
|
+
txnId: '',
|
|
173
|
+
clientId: sub.clientId,
|
|
174
|
+
returnUrl: sub.returnUrl,
|
|
175
|
+
webhookUrl: sub.webhookUrl,
|
|
176
|
+
isSubscription: true,
|
|
177
|
+
subscriptionId: sub.id
|
|
178
|
+
};
|
|
179
|
+
await db.insert(tableNames.TRANSACTION, newTxn);
|
|
180
|
+
if (sub.webhookUrl) {
|
|
181
|
+
try {
|
|
182
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
183
|
+
}
|
|
184
|
+
catch (e) { }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
res.status(200).send({ message: "Subscription webhook processed" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -30,6 +30,38 @@ export interface NPTransaction {
|
|
|
30
30
|
state?: string;
|
|
31
31
|
returnUrl: string;
|
|
32
32
|
webhookUrl: string;
|
|
33
|
+
isSubscription?: boolean;
|
|
34
|
+
subscriptionId?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface NPPlan {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
amount: number;
|
|
41
|
+
currency: string;
|
|
42
|
+
period: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
43
|
+
interval: number;
|
|
44
|
+
trial_days?: number;
|
|
45
|
+
gateway_plan_id?: string;
|
|
46
|
+
clientId: string;
|
|
47
|
+
is_deleted?: boolean;
|
|
48
|
+
createdAt?: number;
|
|
49
|
+
updatedAt?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface NPSubscription {
|
|
52
|
+
id: string;
|
|
53
|
+
planId: string;
|
|
54
|
+
cusId: string;
|
|
55
|
+
status: string;
|
|
56
|
+
gateway_subscription_id?: string;
|
|
57
|
+
short_url?: string;
|
|
58
|
+
clientId: string;
|
|
59
|
+
returnUrl?: string;
|
|
60
|
+
webhookUrl?: string;
|
|
61
|
+
createdAt?: number;
|
|
62
|
+
updatedAt?: number;
|
|
63
|
+
expire_by?: number;
|
|
64
|
+
start_at?: number;
|
|
33
65
|
}
|
|
34
66
|
export interface NPCallbacks {
|
|
35
67
|
onStart: (orderId: string, paymentData?: NPTransaction) => void;
|
|
@@ -84,6 +116,8 @@ export type NPConfigOverrides = {
|
|
|
84
116
|
export type NPTableNames = {
|
|
85
117
|
USER: string;
|
|
86
118
|
TRANSACTION: string;
|
|
119
|
+
PLAN: string;
|
|
120
|
+
SUBSCRIPTION: string;
|
|
87
121
|
};
|
|
88
122
|
export type NPParam = {
|
|
89
123
|
ORDER_ID?: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const subscription_controller_1 = require("../controllers/subscription.controller");
|
|
4
|
+
const subscriptionRoute = function (app, express, callbacks) {
|
|
5
|
+
const config = app.get('np_config');
|
|
6
|
+
const sc = new subscription_controller_1.SubscriptionController(config, app);
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
// Plan Management
|
|
9
|
+
router.post('/plans', (req, res) => sc.createPlan(req, res));
|
|
10
|
+
router.get('/plans', (req, res) => sc.getPlans(req, res));
|
|
11
|
+
router.get('/plans/:id', (req, res) => sc.getPlan(req, res));
|
|
12
|
+
router.patch('/plans/:id', (req, res) => sc.updatePlan(req, res));
|
|
13
|
+
router.delete('/plans/:id', (req, res) => sc.deletePlan(req, res));
|
|
14
|
+
// Subscription Management
|
|
15
|
+
router.post('/init', (req, res) => sc.initSubscription(req, res));
|
|
16
|
+
router.post('/createTxn', (req, res) => sc.initSubscription(req, res));
|
|
17
|
+
router.post('/createTxn/token', (req, res) => sc.initSubscription(req, res));
|
|
18
|
+
router.get('/checkout/:id', (req, res) => sc.checkoutSubscription(req, res));
|
|
19
|
+
router.get('/:id', (req, res) => sc.getSubscription(req, res));
|
|
20
|
+
router.post('/:id/cancel', (req, res) => sc.cancelSubscription(req, res));
|
|
21
|
+
router.get('/:id/payments', (req, res) => sc.getSubscriptionPayments(req, res));
|
|
22
|
+
return router;
|
|
23
|
+
};
|
|
24
|
+
exports.default = subscriptionRoute;
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
24
24
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
25
25
|
const express_handlebars_1 = __importDefault(require("express-handlebars"));
|
|
26
26
|
const payment_controller_1 = require("./app/controllers/payment.controller");
|
|
27
|
+
const subscription_controller_1 = require("./app/controllers/subscription.controller");
|
|
27
28
|
const buildConfig_1 = require("./app/utils/buildConfig");
|
|
28
29
|
__exportStar(require("./app/models"), exports);
|
|
29
30
|
function attachRawBodyAndEngine(app, userConfig = {}) {
|
|
@@ -67,6 +68,7 @@ function createPaymentMiddleware(app, userConfig, db, callbacks, authenticationM
|
|
|
67
68
|
subApp.use(body_parser_1.default.json({ verify: saveRawBody }));
|
|
68
69
|
callbacks = callbacks || config.callbacks;
|
|
69
70
|
const pc = new payment_controller_1.PaymentController(config, db, callbacks, tableNames);
|
|
71
|
+
const sc = new subscription_controller_1.SubscriptionController(config, db, tableNames);
|
|
70
72
|
subApp.use((req, res, next) => {
|
|
71
73
|
let _client = (0, buildConfig_1.withClientConfigOverrides)(config, req);
|
|
72
74
|
const theme = _client.theme || {};
|
|
@@ -88,6 +90,7 @@ function createPaymentMiddleware(app, userConfig, db, callbacks, authenticationM
|
|
|
88
90
|
console.log('Received request at', req.originalUrl);
|
|
89
91
|
next();
|
|
90
92
|
});
|
|
93
|
+
// Payment Routes
|
|
91
94
|
subApp.all('/init', authenticationMiddleware, (req, res) => {
|
|
92
95
|
pc.init(req, res);
|
|
93
96
|
});
|
|
@@ -109,6 +112,19 @@ function createPaymentMiddleware(app, userConfig, db, callbacks, authenticationM
|
|
|
109
112
|
subApp.all('/api/createTxn', authenticationMiddleware, (req, res) => {
|
|
110
113
|
pc.createTxn(req, res);
|
|
111
114
|
});
|
|
115
|
+
// Subscription Routes
|
|
116
|
+
subApp.post('/api/plans', authenticationMiddleware, (req, res) => sc.createPlan(req, res));
|
|
117
|
+
subApp.get('/api/plans', authenticationMiddleware, (req, res) => sc.getPlans(req, res));
|
|
118
|
+
subApp.get('/api/plans/:id', authenticationMiddleware, (req, res) => sc.getPlan(req, res));
|
|
119
|
+
subApp.patch('/api/plans/:id', authenticationMiddleware, (req, res) => sc.updatePlan(req, res));
|
|
120
|
+
subApp.delete('/api/plans/:id', authenticationMiddleware, (req, res) => sc.deletePlan(req, res));
|
|
121
|
+
subApp.post('/api/sub/init', authenticationMiddleware, (req, res) => sc.initSubscription(req, res));
|
|
122
|
+
subApp.post('/api/sub/createTxn', authenticationMiddleware, (req, res) => sc.initSubscription(req, res));
|
|
123
|
+
subApp.post('/api/sub/createTxn/token', authenticationMiddleware, (req, res) => sc.initSubscription(req, res));
|
|
124
|
+
subApp.all('/sub/checkout/:id', (req, res) => sc.checkoutSubscription(req, res));
|
|
125
|
+
subApp.get('/api/sub/:id', authenticationMiddleware, (req, res) => sc.getSubscription(req, res));
|
|
126
|
+
subApp.post('/api/sub/:id/cancel', authenticationMiddleware, (req, res) => sc.cancelSubscription(req, res));
|
|
127
|
+
subApp.get('/api/sub/:id/payments', authenticationMiddleware, (req, res) => sc.getSubscriptionPayments(req, res));
|
|
112
128
|
subApp.all('/', authenticationMiddleware, (req, res) => {
|
|
113
129
|
pc.init(req, res);
|
|
114
130
|
});
|
package/dist/package.json
CHANGED