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.
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
- await this.db.update(this.tableNames.TRANSACTION, myquery, objForUpdate);
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
- const objForUpdate = await this.getOrder(req);
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
- let orderid = req.body.razorpay_order_id || req.query.ORDERID || req.query.order_id;
682
- let liveResonse = null;
683
- if (orderid) {
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 = req.body.razorpay_order_id;
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
- if (req.body.error && req.body.error.metadata && JSON.parse(req.body.error.metadata)) {
697
- const orderId = JSON.parse(req.body.error.metadata).order_id;
698
- req.body.razorpay_order_id = orderId;
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
- if (result || isCancelled) {
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 = ["payment.captured", "payment.pending", "payment.failed"];
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,2 @@
1
+ declare const subscriptionRoute: (app: any, express: any, callbacks?: any) => any;
2
+ export default subscriptionRoute;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-paytmpg",
3
- "version": "7.5.19",
3
+ "version": "8.0.4",
4
4
  "description": "Payment Gateway Integration using NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-paytmpg",
3
- "version": "7.5.19",
3
+ "version": "8.0.4",
4
4
  "description": "Payment Gateway Integration using NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",