node-paytmpg 7.5.19 → 8.0.2
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 +245 -19
- package/dist/app/controllers/subscription.controller.d.ts +22 -0
- package/dist/app/controllers/subscription.controller.js +394 -0
- package/dist/app/controllers/subscription.webhook.d.ts +4 -0
- package/dist/app/controllers/subscription.webhook.js +153 -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>
|
|
@@ -62,7 +62,7 @@ function makeid(length) {
|
|
|
62
62
|
}
|
|
63
63
|
class PaymentController {
|
|
64
64
|
constructor(baseConfig, db, callbacks, tableNames) {
|
|
65
|
-
this.tableNames = { USER: 'npusers', TRANSACTION: 'nptransactions' };
|
|
65
|
+
this.tableNames = { USER: 'npusers', TRANSACTION: 'nptransactions', PLAN: 'npplans', SUBSCRIPTION: 'npsubscriptions' };
|
|
66
66
|
this.viewPath = '';
|
|
67
67
|
this.baseConfig = baseConfig;
|
|
68
68
|
this.callbacks = callbacks;
|
|
@@ -515,12 +515,35 @@ class PaymentController {
|
|
|
515
515
|
const orderToFind = req.body.ORDERID || req.body.ORDER_ID || req.body.ORDERId || (req.query && req.query.order_id) || req.body.ORDER_ID;
|
|
516
516
|
const myquery = { orderId: orderToFind };
|
|
517
517
|
let objForUpdate = null;
|
|
518
|
+
let isSubscription = false;
|
|
518
519
|
try {
|
|
519
520
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, myquery).catch(() => null);
|
|
520
521
|
if (!objForUpdate)
|
|
521
522
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, { id: orderToFind }).catch(() => null);
|
|
522
523
|
if (!objForUpdate)
|
|
523
524
|
objForUpdate = await this.db.getOne(this.tableNames.TRANSACTION, { ORDERID: orderToFind }).catch(() => null);
|
|
525
|
+
if (!objForUpdate) {
|
|
526
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id: orderToFind }).catch(() => null);
|
|
527
|
+
if (sub) {
|
|
528
|
+
isSubscription = true;
|
|
529
|
+
objForUpdate = {
|
|
530
|
+
id: sub.id,
|
|
531
|
+
orderId: sub.id,
|
|
532
|
+
cusId: sub.cusId,
|
|
533
|
+
time: sub.createdAt || Date.now(),
|
|
534
|
+
status: 'INITIATED',
|
|
535
|
+
name: '',
|
|
536
|
+
email: '',
|
|
537
|
+
phone: '',
|
|
538
|
+
amount: 0,
|
|
539
|
+
pname: 'Subscription',
|
|
540
|
+
extra: '',
|
|
541
|
+
clientId: sub.clientId,
|
|
542
|
+
returnUrl: sub.returnUrl || '',
|
|
543
|
+
webhookUrl: sub.webhookUrl || ''
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
524
547
|
}
|
|
525
548
|
catch {
|
|
526
549
|
objForUpdate = objForUpdate || null;
|
|
@@ -586,7 +609,20 @@ class PaymentController {
|
|
|
586
609
|
objForUpdate.txnId = req.body.TXNID;
|
|
587
610
|
objForUpdate.extra = JSON.stringify(req.body);
|
|
588
611
|
try {
|
|
589
|
-
|
|
612
|
+
if (isSubscription) {
|
|
613
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id: orderToFind }, { status: 'AUTHENTICATED', updatedAt: Date.now() });
|
|
614
|
+
// Also persist a transaction record so status/history APIs find it
|
|
615
|
+
const existingTxn = await this.db.getOne(this.tableNames.TRANSACTION, { orderId: orderToFind }).catch(() => null);
|
|
616
|
+
if (!existingTxn) {
|
|
617
|
+
await this.db.insert(this.tableNames.TRANSACTION, objForUpdate);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
await this.db.update(this.tableNames.TRANSACTION, { orderId: orderToFind }, objForUpdate);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
await this.db.update(this.tableNames.TRANSACTION, myquery, objForUpdate);
|
|
625
|
+
}
|
|
590
626
|
}
|
|
591
627
|
catch {
|
|
592
628
|
if (returnUrl) {
|
|
@@ -655,7 +691,32 @@ class PaymentController {
|
|
|
655
691
|
}
|
|
656
692
|
let result = false;
|
|
657
693
|
let isCancelled = false;
|
|
658
|
-
|
|
694
|
+
let objForUpdate = await this.getOrder(req);
|
|
695
|
+
let isSubscriptionCallback = false;
|
|
696
|
+
// Razorpay Subscription Callback Handling
|
|
697
|
+
if (!objForUpdate && req.body.razorpay_subscription_id) {
|
|
698
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { gateway_subscription_id: req.body.razorpay_subscription_id });
|
|
699
|
+
if (sub) {
|
|
700
|
+
isSubscriptionCallback = true;
|
|
701
|
+
// Create a virtual transaction object for the callback processor
|
|
702
|
+
objForUpdate = {
|
|
703
|
+
id: sub.id,
|
|
704
|
+
orderId: sub.id,
|
|
705
|
+
cusId: sub.cusId,
|
|
706
|
+
time: Date.now(),
|
|
707
|
+
status: 'INITIATED',
|
|
708
|
+
name: '',
|
|
709
|
+
email: '',
|
|
710
|
+
phone: '',
|
|
711
|
+
amount: 0,
|
|
712
|
+
pname: 'Subscription Authentication',
|
|
713
|
+
extra: '',
|
|
714
|
+
clientId: sub.clientId,
|
|
715
|
+
returnUrl: sub.returnUrl || '',
|
|
716
|
+
webhookUrl: sub.webhookUrl || ''
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
659
720
|
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, objForUpdate);
|
|
660
721
|
const payuInstance = this.getProviderInstance('PayU', (0, buildConfig_1.withClientConfigOverrides)(config, req, objForUpdate));
|
|
661
722
|
const openMoneyInstance = this.getProviderInstance('OpenMoney', (0, buildConfig_1.withClientConfigOverrides)(config, req, objForUpdate));
|
|
@@ -678,28 +739,42 @@ class PaymentController {
|
|
|
678
739
|
}
|
|
679
740
|
}
|
|
680
741
|
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);
|
|
742
|
+
if (isSubscriptionCallback) {
|
|
743
|
+
// Subscription verification: Razorpay expects payment_id + "|" + subscription_id
|
|
744
|
+
result = checksum_1.default.checkRazorSignature(req.body.razorpay_payment_id, req.body.razorpay_subscription_id, config.SECRET, req.body.razorpay_signature);
|
|
689
745
|
if (result) {
|
|
690
746
|
req.body.STATUS = 'TXN_SUCCESS';
|
|
691
|
-
req.body.ORDERID =
|
|
747
|
+
req.body.ORDERID = objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id;
|
|
692
748
|
req.body.TXNID = req.body.razorpay_payment_id;
|
|
749
|
+
// Update local subscription status
|
|
750
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id: objForUpdate === null || objForUpdate === void 0 ? void 0 : objForUpdate.id }, { status: 'AUTHENTICATED', updatedAt: Date.now() });
|
|
693
751
|
}
|
|
694
752
|
}
|
|
695
753
|
else {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
754
|
+
// Standard Order verification
|
|
755
|
+
let orderid = req.body.razorpay_order_id || req.query.ORDERID || req.query.order_id;
|
|
756
|
+
let liveResonse = null;
|
|
757
|
+
if (orderid) {
|
|
758
|
+
liveResonse = await this.getProviderInstance('Razorpay', config).orders.fetch(orderid).catch(() => null);
|
|
759
|
+
req.body.extras = liveResonse;
|
|
760
|
+
}
|
|
761
|
+
if (req.body.razorpay_payment_id) {
|
|
762
|
+
result = checksum_1.default.checkRazorSignature(req.body.razorpay_order_id, req.body.razorpay_payment_id, config.SECRET, req.body.razorpay_signature);
|
|
763
|
+
if (result) {
|
|
764
|
+
req.body.STATUS = 'TXN_SUCCESS';
|
|
765
|
+
req.body.ORDERID = req.body.razorpay_order_id;
|
|
766
|
+
req.body.TXNID = req.body.razorpay_payment_id;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
if (req.body.error && req.body.error.metadata && JSON.parse(req.body.error.metadata)) {
|
|
771
|
+
const orderId = JSON.parse(req.body.error.metadata).order_id;
|
|
772
|
+
req.body.razorpay_order_id = orderId;
|
|
773
|
+
}
|
|
774
|
+
req.body.STATUS = (liveResonse === null || liveResonse === void 0 ? void 0 : liveResonse.attempts) ? 'TXN_FAILURE' : 'CANCELLED';
|
|
775
|
+
req.body.ORDERID = req.body.razorpay_order_id || req.query.order_id;
|
|
776
|
+
isCancelled = true;
|
|
699
777
|
}
|
|
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
778
|
}
|
|
704
779
|
}
|
|
705
780
|
else if (config.payu_url) {
|
|
@@ -750,6 +825,7 @@ class PaymentController {
|
|
|
750
825
|
return serviceUsed;
|
|
751
826
|
}
|
|
752
827
|
async webhook(req, res) {
|
|
828
|
+
var _a;
|
|
753
829
|
try {
|
|
754
830
|
let config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
755
831
|
const payuInstance = this.getProviderInstance('PayU', config);
|
|
@@ -763,8 +839,158 @@ class PaymentController {
|
|
|
763
839
|
return;
|
|
764
840
|
}
|
|
765
841
|
if (serviceUsed === 'Razorpay') {
|
|
766
|
-
const events = [
|
|
842
|
+
const events = [
|
|
843
|
+
"payment.captured", "payment.pending", "payment.failed",
|
|
844
|
+
"subscription.authenticated", "subscription.paused", "subscription.resumed",
|
|
845
|
+
"subscription.activated", "subscription.pending", "subscription.halted",
|
|
846
|
+
"subscription.charged", "subscription.cancelled", "subscription.completed",
|
|
847
|
+
"subscription.updated"
|
|
848
|
+
];
|
|
767
849
|
if (req.body.event && events.indexOf(req.body.event) > -1) {
|
|
850
|
+
const event = req.body.event;
|
|
851
|
+
// Handle Subscription Events
|
|
852
|
+
if (event.startsWith("subscription.")) {
|
|
853
|
+
if (req.body.payload && req.body.payload.subscription && req.body.payload.subscription.entity) {
|
|
854
|
+
const subEntity = req.body.payload.subscription.entity;
|
|
855
|
+
const paymentEntity = (_a = req.body.payload.payment) === null || _a === void 0 ? void 0 : _a.entity;
|
|
856
|
+
const gateway_subscription_id = subEntity.id;
|
|
857
|
+
const reqBody = req.rawBody;
|
|
858
|
+
const signature = req.headers["x-razorpay-signature"];
|
|
859
|
+
if (signature === undefined) {
|
|
860
|
+
res.status(200).send({ message: "Missing Razorpay signature" });
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
let signatureValid;
|
|
864
|
+
try {
|
|
865
|
+
signatureValid = razorpay_1.default.validateWebhookSignature(reqBody, signature, config.SECRET);
|
|
866
|
+
}
|
|
867
|
+
catch (e) {
|
|
868
|
+
signatureValid = false;
|
|
869
|
+
}
|
|
870
|
+
if (!signatureValid) {
|
|
871
|
+
res.status(200).send({ message: "Invalid Rzpay signature" });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
// Find the local subscription
|
|
875
|
+
const sub = await this.db.getOne(this.tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { gateway_subscription_id });
|
|
876
|
+
if (!sub) {
|
|
877
|
+
console.log("Subscription not found for webhook:", gateway_subscription_id);
|
|
878
|
+
res.status(200).send({ message: "Subscription not found locally" });
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const clientConf = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, { clientId: sub.clientId });
|
|
882
|
+
let statusChanged = false;
|
|
883
|
+
// Map Razorpay events to local subscription status
|
|
884
|
+
switch (event) {
|
|
885
|
+
case "subscription.authenticated":
|
|
886
|
+
sub.status = 'AUTHENTICATED';
|
|
887
|
+
statusChanged = true;
|
|
888
|
+
break;
|
|
889
|
+
case "subscription.activated":
|
|
890
|
+
case "subscription.resumed":
|
|
891
|
+
case "subscription.updated": // An update might make it active again or just change metadata
|
|
892
|
+
if (subEntity.status === 'active') {
|
|
893
|
+
sub.status = 'ACTIVE';
|
|
894
|
+
statusChanged = true;
|
|
895
|
+
}
|
|
896
|
+
break;
|
|
897
|
+
case "subscription.paused":
|
|
898
|
+
sub.status = 'PAUSED';
|
|
899
|
+
statusChanged = true;
|
|
900
|
+
break;
|
|
901
|
+
case "subscription.pending":
|
|
902
|
+
sub.status = 'PENDING';
|
|
903
|
+
statusChanged = true;
|
|
904
|
+
break;
|
|
905
|
+
case "subscription.halted":
|
|
906
|
+
sub.status = 'HALTED';
|
|
907
|
+
statusChanged = true;
|
|
908
|
+
break;
|
|
909
|
+
case "subscription.cancelled":
|
|
910
|
+
sub.status = 'CANCELLED';
|
|
911
|
+
statusChanged = true;
|
|
912
|
+
break;
|
|
913
|
+
case "subscription.completed":
|
|
914
|
+
sub.status = 'COMPLETED';
|
|
915
|
+
statusChanged = true;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
if (statusChanged) {
|
|
919
|
+
sub.updatedAt = Date.now();
|
|
920
|
+
await this.db.update(this.tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
921
|
+
}
|
|
922
|
+
// Trigger client payment webhook ONLY on actual charges or definitive failures
|
|
923
|
+
if (event === "subscription.charged" && paymentEntity) {
|
|
924
|
+
sub.status = 'ACTIVE';
|
|
925
|
+
await this.db.update(this.tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
926
|
+
// Create a new transaction record for this specific charge
|
|
927
|
+
const txnId = 'txn_' + makeid(10);
|
|
928
|
+
const newTxn = {
|
|
929
|
+
id: txnId,
|
|
930
|
+
orderId: txnId, // Use txnId as orderId for recurring payments since there is no explicit user-created order
|
|
931
|
+
cusId: sub.cusId,
|
|
932
|
+
time: Date.now(),
|
|
933
|
+
status: 'TXN_SUCCESS',
|
|
934
|
+
name: '', // We could fetch from user, but keeping minimal
|
|
935
|
+
email: paymentEntity.email || '',
|
|
936
|
+
phone: paymentEntity.contact || '',
|
|
937
|
+
amount: paymentEntity.amount / 100,
|
|
938
|
+
pname: 'Subscription Charge',
|
|
939
|
+
extra: JSON.stringify(paymentEntity),
|
|
940
|
+
txnId: paymentEntity.id,
|
|
941
|
+
clientId: sub.clientId,
|
|
942
|
+
returnUrl: sub.returnUrl,
|
|
943
|
+
webhookUrl: sub.webhookUrl,
|
|
944
|
+
isSubscription: true,
|
|
945
|
+
subscriptionId: sub.id
|
|
946
|
+
};
|
|
947
|
+
await this.db.insert(this.tableNames.TRANSACTION, newTxn);
|
|
948
|
+
// Trigger client webhook
|
|
949
|
+
if (sub.webhookUrl) {
|
|
950
|
+
try {
|
|
951
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
952
|
+
console.log("Sent subscription webhook to ", sub.webhookUrl, 'txnId:', paymentEntity.id);
|
|
953
|
+
}
|
|
954
|
+
catch (e) {
|
|
955
|
+
console.log("Error sending subscription webhook to ", sub.webhookUrl, (e === null || e === void 0 ? void 0 : e.message) || e);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else if (event === "subscription.halted") {
|
|
960
|
+
// Optional: Inform client of a failed recurring payment that led to a halt
|
|
961
|
+
const txnId = 'txn_' + makeid(10);
|
|
962
|
+
const newTxn = {
|
|
963
|
+
id: txnId,
|
|
964
|
+
orderId: txnId,
|
|
965
|
+
cusId: sub.cusId,
|
|
966
|
+
time: Date.now(),
|
|
967
|
+
status: 'TXN_FAILURE',
|
|
968
|
+
name: '',
|
|
969
|
+
email: '',
|
|
970
|
+
phone: '',
|
|
971
|
+
amount: 0, // Or fetch plan amount if needed
|
|
972
|
+
pname: 'Subscription Halted',
|
|
973
|
+
extra: JSON.stringify(subEntity),
|
|
974
|
+
txnId: '',
|
|
975
|
+
clientId: sub.clientId,
|
|
976
|
+
returnUrl: sub.returnUrl,
|
|
977
|
+
webhookUrl: sub.webhookUrl,
|
|
978
|
+
isSubscription: true,
|
|
979
|
+
subscriptionId: sub.id
|
|
980
|
+
};
|
|
981
|
+
await this.db.insert(this.tableNames.TRANSACTION, newTxn);
|
|
982
|
+
if (sub.webhookUrl) {
|
|
983
|
+
try {
|
|
984
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
985
|
+
}
|
|
986
|
+
catch (e) { }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
res.status(200).send({ message: "Subscription webhook processed" });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// Handle One-time Payment Events
|
|
768
994
|
if (req.body.payload &&
|
|
769
995
|
req.body.payload.payment &&
|
|
770
996
|
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,394 @@
|
|
|
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 } = 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
|
+
needsNewGatewayPlan = true;
|
|
145
|
+
}
|
|
146
|
+
const updatedPlan = { ...plan, updatedAt: Date.now() };
|
|
147
|
+
if (name !== undefined)
|
|
148
|
+
updatedPlan.name = name;
|
|
149
|
+
if (description !== undefined)
|
|
150
|
+
updatedPlan.description = description;
|
|
151
|
+
if (amount !== undefined)
|
|
152
|
+
updatedPlan.amount = parseFloat(amount);
|
|
153
|
+
if (interval !== undefined)
|
|
154
|
+
updatedPlan.interval = parseInt(interval, 10);
|
|
155
|
+
if (period !== undefined)
|
|
156
|
+
updatedPlan.period = period;
|
|
157
|
+
if (needsNewGatewayPlan) {
|
|
158
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
159
|
+
const provider = this.getProvider(config);
|
|
160
|
+
if (provider) {
|
|
161
|
+
try {
|
|
162
|
+
const newGatewayId = await provider.createPlan(updatedPlan, config);
|
|
163
|
+
updatedPlan.gateway_plan_id = newGatewayId;
|
|
164
|
+
}
|
|
165
|
+
catch (gwErr) {
|
|
166
|
+
res.status(500).send({ message: 'Failed to create updated plan on Gateway', error: gwErr === null || gwErr === void 0 ? void 0 : gwErr.message });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
await this.db.update(this.tableNames.PLAN, { id }, updatedPlan);
|
|
172
|
+
res.send(updatedPlan);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
res.status(500).send({ message: 'Error updating plan', error: err === null || err === void 0 ? void 0 : err.message });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async deletePlan(req, res) {
|
|
179
|
+
try {
|
|
180
|
+
const id = req.params.id;
|
|
181
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id });
|
|
182
|
+
if (!plan) {
|
|
183
|
+
res.status(404).send({ message: 'Plan not found' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Soft delete
|
|
187
|
+
await this.db.update(this.tableNames.PLAN, { id }, { ...plan, is_deleted: true, updatedAt: Date.now() });
|
|
188
|
+
res.send({ message: 'Plan deleted successfully', id });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
res.status(500).send({ message: 'Error deleting plan', error: err === null || err === void 0 ? void 0 : err.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// --- SUBSCRIPTION MANAGEMENT ---
|
|
195
|
+
async initSubscription(req, res) {
|
|
196
|
+
var _a;
|
|
197
|
+
try {
|
|
198
|
+
const { planId, returnUrl, webhookUrl, NAME, EMAIL, MOBILE_NO, CLIENT_ID } = req.body;
|
|
199
|
+
if (!planId || !NAME || !EMAIL) {
|
|
200
|
+
res.status(400).send({ message: 'Missing required fields: planId, NAME, EMAIL' });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: planId, is_deleted: false });
|
|
204
|
+
if (!plan || !plan.gateway_plan_id) {
|
|
205
|
+
res.status(404).send({ message: 'Active plan not found or not synced with gateway.' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
209
|
+
const provider = this.getProvider(config);
|
|
210
|
+
if (!provider) {
|
|
211
|
+
res.status(400).send({ message: 'No supported subscription provider configured.' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Create/Get User
|
|
215
|
+
const user = await this.userController.create({ name: NAME, email: EMAIL, phone: MOBILE_NO });
|
|
216
|
+
const subId = 'sub_' + utils_1.Utils.makeid(14);
|
|
217
|
+
const subData = {
|
|
218
|
+
id: subId,
|
|
219
|
+
planId: plan.id,
|
|
220
|
+
cusId: user.id,
|
|
221
|
+
status: 'CREATED',
|
|
222
|
+
clientId: CLIENT_ID || req.query.client_id || '',
|
|
223
|
+
returnUrl: returnUrl || '',
|
|
224
|
+
webhookUrl: webhookUrl || '',
|
|
225
|
+
createdAt: Date.now(),
|
|
226
|
+
updatedAt: Date.now()
|
|
227
|
+
};
|
|
228
|
+
// Call Gateway
|
|
229
|
+
try {
|
|
230
|
+
const { id: gateway_sub_id, url: short_url } = await provider.createSubscription(subData, plan, config);
|
|
231
|
+
subData.gateway_subscription_id = gateway_sub_id;
|
|
232
|
+
subData.short_url = short_url;
|
|
233
|
+
}
|
|
234
|
+
catch (gwErr) {
|
|
235
|
+
console.error("Gateway Sub Error:", gwErr);
|
|
236
|
+
res.status(500).send({ message: 'Failed to initialize subscription on gateway', error: gwErr === null || gwErr === void 0 ? void 0 : gwErr.message });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await this.db.insert(this.tableNames.SUBSCRIPTION, subData);
|
|
240
|
+
const responseData = {
|
|
241
|
+
...subData,
|
|
242
|
+
orderId: subData.id,
|
|
243
|
+
payurl: config.host_url + '/' + config.path_prefix + '/sub/checkout/' + subData.id,
|
|
244
|
+
pname: plan.name,
|
|
245
|
+
amount: plan.amount,
|
|
246
|
+
name: user.name,
|
|
247
|
+
email: user.email,
|
|
248
|
+
phone: user.phone
|
|
249
|
+
};
|
|
250
|
+
if (((_a = req.headers.accept) === null || _a === void 0 ? void 0 : _a.includes('application/json')) || req.path.includes('/createTxn')) {
|
|
251
|
+
res.status(201).send(responseData);
|
|
252
|
+
}
|
|
253
|
+
else if (subData.short_url) {
|
|
254
|
+
res.redirect(config.host_url + '/' + config.path_prefix + '/sub/checkout/' + subData.id);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
res.status(201).send(responseData); // fallback
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
console.error("Init Sub Error:", err);
|
|
262
|
+
res.status(500).send({ message: 'Internal server error', error: err === null || err === void 0 ? void 0 : err.message });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async checkoutSubscription(req, res) {
|
|
266
|
+
try {
|
|
267
|
+
const id = req.params.id;
|
|
268
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
269
|
+
if (!sub || !sub.gateway_subscription_id) {
|
|
270
|
+
res.status(404).send({ message: 'Subscription not found or not ready.' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const plan = await this.db.getOne(this.tableNames.PLAN, { id: sub.planId });
|
|
274
|
+
const user = await this.db.getOne(this.tableNames.USER, { id: sub.cusId });
|
|
275
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, { clientId: sub.clientId });
|
|
276
|
+
const params = {
|
|
277
|
+
ORDER_ID: sub.gateway_subscription_id,
|
|
278
|
+
CALLBACK_URL: config.host_url + '/' + config.path_prefix + '/callback',
|
|
279
|
+
NAME: (user === null || user === void 0 ? void 0 : user.name) || '',
|
|
280
|
+
EMAIL: (user === null || user === void 0 ? void 0 : user.email) || '',
|
|
281
|
+
MOBILE_NO: (user === null || user === void 0 ? void 0 : user.phone) || '',
|
|
282
|
+
PRODUCT_NAME: (plan === null || plan === void 0 ? void 0 : plan.name) || ''
|
|
283
|
+
};
|
|
284
|
+
(0, htmlhelper_1.renderRazorpayCheckout)(req, res, params, config, loadingsvg_1.LoadingSVG, true);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
res.status(500).send({ message: 'Error rendering checkout', error: err === null || err === void 0 ? void 0 : err.message });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async getSubscription(req, res) {
|
|
291
|
+
try {
|
|
292
|
+
const id = req.params.id;
|
|
293
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
294
|
+
if (!sub) {
|
|
295
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Optionally sync from provider
|
|
299
|
+
if (req.query.sync && sub.gateway_subscription_id) {
|
|
300
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
301
|
+
const provider = this.getProvider(config);
|
|
302
|
+
if (provider) {
|
|
303
|
+
try {
|
|
304
|
+
const gwData = await provider.getSubscription(sub.gateway_subscription_id, config);
|
|
305
|
+
let newStatus = sub.status;
|
|
306
|
+
if (gwData.status === 'active')
|
|
307
|
+
newStatus = 'ACTIVE';
|
|
308
|
+
else if (gwData.status === 'authenticated')
|
|
309
|
+
newStatus = 'AUTHENTICATED';
|
|
310
|
+
else if (gwData.status === 'cancelled')
|
|
311
|
+
newStatus = 'CANCELLED';
|
|
312
|
+
else if (gwData.status === 'completed')
|
|
313
|
+
newStatus = 'COMPLETED';
|
|
314
|
+
else if (gwData.status === 'expired')
|
|
315
|
+
newStatus = 'EXPIRED';
|
|
316
|
+
else if (gwData.status === 'pending' || gwData.status === 'halted')
|
|
317
|
+
newStatus = 'HALTED';
|
|
318
|
+
if (newStatus !== sub.status) {
|
|
319
|
+
sub.status = newStatus;
|
|
320
|
+
sub.updatedAt = Date.now();
|
|
321
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id }, sub);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (gwErr) {
|
|
325
|
+
console.error('Failed to sync sub status:', gwErr);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
res.send(sub);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
res.status(500).send({ message: 'Error fetching subscription', error: err === null || err === void 0 ? void 0 : err.message });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async cancelSubscription(req, res) {
|
|
336
|
+
try {
|
|
337
|
+
const id = req.params.id;
|
|
338
|
+
const cancelAtCycleEnd = req.body.cancel_at_cycle_end === true || req.body.cancel_at_cycle_end === 'true';
|
|
339
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
340
|
+
if (!sub) {
|
|
341
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (sub.status === 'CANCELLED' || sub.status === 'EXPIRED' || sub.status === 'COMPLETED') {
|
|
345
|
+
res.status(400).send({ message: `Cannot cancel subscription in ${sub.status} state` });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const config = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req);
|
|
349
|
+
const provider = this.getProvider(config);
|
|
350
|
+
if (provider && sub.gateway_subscription_id) {
|
|
351
|
+
try {
|
|
352
|
+
await provider.cancelSubscription(sub.gateway_subscription_id, cancelAtCycleEnd, config);
|
|
353
|
+
if (!cancelAtCycleEnd) {
|
|
354
|
+
sub.status = 'CANCELLED';
|
|
355
|
+
}
|
|
356
|
+
sub.updatedAt = Date.now();
|
|
357
|
+
await this.db.update(this.tableNames.SUBSCRIPTION, { id }, sub);
|
|
358
|
+
res.send({ message: 'Cancellation processed successfully', status: sub.status });
|
|
359
|
+
}
|
|
360
|
+
catch (gwErr) {
|
|
361
|
+
res.status(500).send({ message: 'Failed to cancel on gateway', error: (gwErr === null || gwErr === void 0 ? void 0 : gwErr.message) || gwErr });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
res.status(400).send({ message: 'No provider configured or missing gateway subscription ID' });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
res.status(500).send({ message: 'Error cancelling subscription', error: err === null || err === void 0 ? void 0 : err.message });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async getSubscriptionPayments(req, res) {
|
|
373
|
+
try {
|
|
374
|
+
const id = req.params.id;
|
|
375
|
+
const sub = await this.db.getOne(this.tableNames.SUBSCRIPTION, { id });
|
|
376
|
+
if (!sub) {
|
|
377
|
+
res.status(404).send({ message: 'Subscription not found' });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
381
|
+
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
|
382
|
+
// Fetch transactions linked to this subscription
|
|
383
|
+
const payments = await this.db.get(this.tableNames.TRANSACTION, { subscriptionId: id }, {
|
|
384
|
+
sort: [{ field: 'time', order: 'desc' }],
|
|
385
|
+
limit: limit, offset: offset
|
|
386
|
+
});
|
|
387
|
+
res.send({ limit, offset, count: payments.length, payments });
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
res.status(500).send({ message: 'Error fetching payments', error: err === null || err === void 0 ? void 0 : err.message });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
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,153 @@
|
|
|
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
|
+
break;
|
|
50
|
+
case "subscription.activated":
|
|
51
|
+
case "subscription.resumed":
|
|
52
|
+
case "subscription.updated": // An update might make it active again or just change metadata
|
|
53
|
+
if (subEntity.status === 'active') {
|
|
54
|
+
sub.status = 'ACTIVE';
|
|
55
|
+
statusChanged = true;
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case "subscription.paused":
|
|
59
|
+
sub.status = 'PAUSED';
|
|
60
|
+
statusChanged = true;
|
|
61
|
+
break;
|
|
62
|
+
case "subscription.pending":
|
|
63
|
+
sub.status = 'PENDING';
|
|
64
|
+
statusChanged = true;
|
|
65
|
+
break;
|
|
66
|
+
case "subscription.halted":
|
|
67
|
+
sub.status = 'HALTED';
|
|
68
|
+
statusChanged = true;
|
|
69
|
+
break;
|
|
70
|
+
case "subscription.cancelled":
|
|
71
|
+
sub.status = 'CANCELLED';
|
|
72
|
+
statusChanged = true;
|
|
73
|
+
break;
|
|
74
|
+
case "subscription.completed":
|
|
75
|
+
sub.status = 'COMPLETED';
|
|
76
|
+
statusChanged = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (statusChanged) {
|
|
80
|
+
sub.updatedAt = Date.now();
|
|
81
|
+
await db.update(tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
82
|
+
}
|
|
83
|
+
// Trigger client payment webhook ONLY on actual charges or definitive failures
|
|
84
|
+
if (event === "subscription.charged" && paymentEntity) {
|
|
85
|
+
sub.status = 'ACTIVE';
|
|
86
|
+
await db.update(tableNames.TRANSACTION.replace('transactions', 'subscriptions'), { id: sub.id }, sub);
|
|
87
|
+
// Create a new transaction record for this specific charge
|
|
88
|
+
const txnId = 'txn_' + makeid(10);
|
|
89
|
+
const newTxn = {
|
|
90
|
+
id: txnId,
|
|
91
|
+
orderId: txnId, // Use txnId as orderId for recurring payments since there is no explicit user-created order
|
|
92
|
+
cusId: sub.cusId,
|
|
93
|
+
time: Date.now(),
|
|
94
|
+
status: 'TXN_SUCCESS',
|
|
95
|
+
name: '', // We could fetch from user, but keeping minimal
|
|
96
|
+
email: paymentEntity.email || '',
|
|
97
|
+
phone: paymentEntity.contact || '',
|
|
98
|
+
amount: paymentEntity.amount / 100,
|
|
99
|
+
pname: 'Subscription Charge',
|
|
100
|
+
extra: JSON.stringify(paymentEntity),
|
|
101
|
+
txnId: paymentEntity.id,
|
|
102
|
+
clientId: sub.clientId,
|
|
103
|
+
returnUrl: sub.returnUrl,
|
|
104
|
+
webhookUrl: sub.webhookUrl,
|
|
105
|
+
isSubscription: true,
|
|
106
|
+
subscriptionId: sub.id
|
|
107
|
+
};
|
|
108
|
+
await db.insert(tableNames.TRANSACTION, newTxn);
|
|
109
|
+
// Trigger client webhook
|
|
110
|
+
if (sub.webhookUrl) {
|
|
111
|
+
try {
|
|
112
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
113
|
+
console.log("Sent subscription webhook to ", sub.webhookUrl, 'txnId:', paymentEntity.id);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.log("Error sending subscription webhook to ", sub.webhookUrl, (e === null || e === void 0 ? void 0 : e.message) || e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (event === "subscription.halted") {
|
|
121
|
+
// Optional: Inform client of a failed recurring payment that led to a halt
|
|
122
|
+
const txnId = 'txn_' + makeid(10);
|
|
123
|
+
const newTxn = {
|
|
124
|
+
id: txnId,
|
|
125
|
+
orderId: txnId,
|
|
126
|
+
cusId: sub.cusId,
|
|
127
|
+
time: Date.now(),
|
|
128
|
+
status: 'TXN_FAILURE',
|
|
129
|
+
name: '',
|
|
130
|
+
email: '',
|
|
131
|
+
phone: '',
|
|
132
|
+
amount: 0, // Or fetch plan amount if needed
|
|
133
|
+
pname: 'Subscription Halted',
|
|
134
|
+
extra: JSON.stringify(subEntity),
|
|
135
|
+
txnId: '',
|
|
136
|
+
clientId: sub.clientId,
|
|
137
|
+
returnUrl: sub.returnUrl,
|
|
138
|
+
webhookUrl: sub.webhookUrl,
|
|
139
|
+
isSubscription: true,
|
|
140
|
+
subscriptionId: sub.id
|
|
141
|
+
};
|
|
142
|
+
await db.insert(tableNames.TRANSACTION, newTxn);
|
|
143
|
+
if (sub.webhookUrl) {
|
|
144
|
+
try {
|
|
145
|
+
await axios_1.default.post(sub.webhookUrl, newTxn);
|
|
146
|
+
}
|
|
147
|
+
catch (e) { }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
res.status(200).send({ message: "Subscription webhook processed" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -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