node-paytmpg 7.5.18 → 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.
@@ -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 });
@@ -224,7 +224,7 @@ class PayU {
224
224
  }
225
225
  let updatedConfig = (0, buildConfig_1.withClientConfigOverrides)(this.baseConfig, req, originalOrder);
226
226
  this.initParams(updatedConfig);
227
- const payuRest = await this.verifyResult(req);
227
+ const payuRest = await this.verifyResult(req, orderId);
228
228
  let result = !!payuRest.STATUS;
229
229
  req.body.STATUS = payuRest.STATUS;
230
230
  req.body.TXNID = payuRest.TXNID;
@@ -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
- await this.db.update(this.tableNames.TRANSACTION, myquery, objForUpdate);
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
- const objForUpdate = await this.getOrder(req);
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
- 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);
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 = req.body.razorpay_order_id;
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
- 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;
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 = ["payment.captured", "payment.pending", "payment.failed"];
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,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.18",
3
+ "version": "8.0.2",
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.18",
3
+ "version": "8.0.2",
4
4
  "description": "Payment Gateway Integration using NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",