From c93a4f47185be5ef8639663a0f5f5a2c48bc6abb Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 19:48:37 +0530 Subject: [PATCH 01/81] Update .firebaserc --- .firebaserc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.firebaserc b/.firebaserc index 008864b..74dd134 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,6 +1,7 @@ { "projects": { "debug": "fitlien-dev", + "qa": "fitlien-qa", "release": "fitlien" } -} +} \ No newline at end of file From 0f11768b65a186084c2262bf6d7c1f0d8d9b8c5f Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 19:57:50 +0530 Subject: [PATCH 02/81] Adding QA pipeline --- fitlien-services-qa-pipeline.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 fitlien-services-qa-pipeline.yaml diff --git a/fitlien-services-qa-pipeline.yaml b/fitlien-services-qa-pipeline.yaml new file mode 100644 index 0000000..c9fb9ed --- /dev/null +++ b/fitlien-services-qa-pipeline.yaml @@ -0,0 +1,30 @@ +trigger: + - dev + +pool: + vmImage: "ubuntu-latest" + +variables: + major: $(VERSION_MAJOR) + minor: $(VERSION_MINOR) + prefix: $[format('{0}.{1}', variables['major'], variables['minor'])] + patch: $[counter(variables['prefix'], 100)] + buildNumber: $(major).$(minor).$(patch) + +resources: + repositories: + - repository: templateRepo + endpoint: cosq-network + type: github + name: cosq-network/azure-build-pipeline-templates + +extends: + template: firebase-functions-deploy.yaml@templateRepo + parameters: + nodeVersion: "20" + firebaseTokenSecret: $(FIREBASE_TOKEN) + functionsWorkingDirectory: "$(Build.SourcesDirectory)/functions" + envExamplePath: "$(Build.SourcesDirectory)/functions/.env.example" + envPath: "$(Build.SourcesDirectory)/functions/.env" + buildNumber: $(buildNumber) + buildType: qa From d4090faa82e528f8d11aa3561ce4c45b26b51ccf Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 20:38:32 +0530 Subject: [PATCH 03/81] Update fitlien-services-qa-pipeline.yaml --- fitlien-services-qa-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fitlien-services-qa-pipeline.yaml b/fitlien-services-qa-pipeline.yaml index c9fb9ed..75d9767 100644 --- a/fitlien-services-qa-pipeline.yaml +++ b/fitlien-services-qa-pipeline.yaml @@ -1,5 +1,5 @@ trigger: - - dev + - main pool: vmImage: "ubuntu-latest" From 7d37e295fe7c1b8353e9314192f2aa407b8279fd Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 14:36:46 +0000 Subject: [PATCH 04/81] Added cors createCashfreeOrder (#7) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/7 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/package-lock.json | 1 + functions/package.json | 1 + functions/src/index.ts | 169 +++++++++++++++++++----------------- 3 files changed, 90 insertions(+), 81 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index ac67f4d..8a295b3 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/package.json b/functions/package.json index 5c20815..939628c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -17,6 +17,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/src/index.ts b/functions/src/index.ts index a26adc6..b250098 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; +import cors from 'cors'; import axios from "axios"; import { getStorage } from 'firebase-admin/storage'; const formData = require('form-data'); @@ -19,6 +20,9 @@ const twilio = require('twilio'); if (!admin.apps.length) { admin.initializeApp(); } + +const corsHandler = cors({ origin: true }); + export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { @@ -245,92 +249,95 @@ export const notifyInvitation = onDocumentCreated({ export const createCashfreeOrder = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const authHeader = request.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - response.status(401).json({ error: 'Unauthorized' }); - return; - } - const idToken = authHeader.split('Bearer ')[1]; - const decodedToken = await admin.auth().verifyIdToken(idToken); - const uid = decodedToken.uid; - - const { - amount, - customerName, - customerEmail, - customerPhone, - productInfo - } = request.body; - - if (!amount || !customerEmail || !customerPhone) { - response.status(400).json({ error: 'Missing required fields' }); - return; - } - - const clientId = process.env.CASHFREE_CLIENT_ID; - const clientSecret = process.env.CASHFREE_CLIENT_SECRET; - const isTest = true; - - const apiUrl = isTest - ? 'https://sandbox.cashfree.com/pg/orders' - : 'https://api.cashfree.com/pg/orders'; - - const orderId = `order_${Date.now()}_${uid.substring(0, 6)}`; - - const cashfreeResponse = await axios.post( - apiUrl, - { - order_id: orderId, - order_amount: amount, - order_currency: 'INR', - customer_details: { - customer_id: uid, - customer_name: customerName || 'Fitlien User', - customer_email: customerEmail, - customer_phone: customerPhone - }, - order_meta: { - return_url: `https://fitlien.com/payment/status?order_id={order_id}`, - // notify_url: `https://$filien.web.app/verifyCashfreePayment` - }, - order_note: productInfo || 'Fitlien Membership' - }, - { - headers: { - 'x-api-version': '2022-09-01', - 'x-client-id': clientId, - 'x-client-secret': clientSecret, - 'Content-Type': 'application/json' - } + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; } - ); - await admin.firestore().collection('payment_orders').doc(orderId).set({ - userId: uid, - amount: amount, - customerEmail: customerEmail, - customerPhone: customerPhone, - orderStatus: 'CREATED', - paymentGateway: 'Cashfree', - createdAt: new Date(), - ...cashfreeResponse.data - }); + const idToken = authHeader.split('Bearer ')[1]; + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; - response.json({ - order_id: cashfreeResponse.data.order_id, - payment_session_id: cashfreeResponse.data.payment_session_id - }); + const { + amount, + customerName, + customerEmail, + customerPhone, + productInfo + } = request.body; - logger.info(`Cashfree order created: ${orderId}`); - } catch (error: any) { - logger.error('Cashfree order creation error:', error); - response.status(500).json({ - error: 'Failed to create payment order', - details: error.response?.data || error.message - }); - } + if (!amount || !customerEmail || !customerPhone) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = true; + + const apiUrl = isTest + ? 'https://sandbox.cashfree.com/pg/orders' + : 'https://api.cashfree.com/pg/orders'; + + const orderId = `order_${Date.now()}_${uid.substring(0, 6)}`; + + const cashfreeResponse = await axios.post( + apiUrl, + { + order_id: orderId, + order_amount: amount, + order_currency: 'INR', + customer_details: { + customer_id: uid, + customer_name: customerName || 'Fitlien User', + customer_email: customerEmail, + customer_phone: customerPhone + }, + order_meta: { + return_url: `https://fitlien.com/payment/status?order_id={order_id}`, + // notify_url: `https://$filien.web.app/verifyCashfreePayment` + }, + order_note: productInfo || 'Fitlien Membership' + }, + { + headers: { + 'x-api-version': '2022-09-01', + 'x-client-id': clientId, + 'x-client-secret': clientSecret, + 'Content-Type': 'application/json' + } + } + ); + + await admin.firestore().collection('payment_orders').doc(orderId).set({ + userId: uid, + amount: amount, + customerEmail: customerEmail, + customerPhone: customerPhone, + orderStatus: 'CREATED', + paymentGateway: 'Cashfree', + createdAt: new Date(), + ...cashfreeResponse.data + }); + + response.json({ + order_id: cashfreeResponse.data.order_id, + payment_session_id: cashfreeResponse.data.payment_session_id + }); + + logger.info(`Cashfree order created: ${orderId}`); + } catch (error: any) { + logger.error('Cashfree order creation error:', error); + response.status(500).json({ + error: 'Failed to create payment order', + details: error.response?.data || error.message + }); + } + }); }); export const verifyCashfreePayment = onRequest({ From 0be5cfe30cd631535dc1354346f74db2560f92e3 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 15:08:21 +0000 Subject: [PATCH 05/81] feature/fitlien-add-cors (#8) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/8 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/src/index.ts | 339 +++++++++++++++++++++-------------------- 1 file changed, 177 insertions(+), 162 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index b250098..4c98cab 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -26,147 +26,158 @@ const corsHandler = cors({ origin: true }); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const { toAddress, subject, message, fileUrl, fileName } = request.body; + return corsHandler(request, response, async () => { + try { + const { toAddress, subject, message, fileUrl, fileName } = request.body; - if (!toAddress || !subject || !message || !fileUrl) { - response.status(400).json({ - error: 'Missing required fields (toAddress, subject, message, fileUrl)' - }); - return; - } - const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(tempFilePath); - https.get(fileUrl, (res) => { - res.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); + if (!toAddress || !subject || !message || !fileUrl) { + response.status(400).json({ + error: 'Missing required fields (toAddress, subject, message, fileUrl)' }); - }).on('error', (err) => { - fs.unlink(tempFilePath, () => { }); - reject(err); - }); - }); - - const mailgun = new Mailgun(formData); - const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - - const options = { - wordwrap: 130, - }; - const textMessage = convert(message, options); - const fileBuffer = fs.readFileSync(tempFilePath); - const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); - - const data = { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message, - attachment: { - data: fileBuffer, - filename: attachmentFilename, - contentType: 'application/pdf', + return; } - }; + const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); + await new Promise((resolve, reject) => { + const file = fs.createWriteStream(tempFilePath); + https.get(fileUrl, (res) => { + res.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', (err) => { + fs.unlink(tempFilePath, () => { }); + reject(err); + }); + }); - const result = await client.messages.create(process.env.MAILGUN_SERVER, data); - fs.unlinkSync(tempFilePath); + const mailgun = new Mailgun(formData); + const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - logger.info('Email with attachment from URL sent successfully'); - response.json({ success: true, result }); - } catch (error) { - logger.error('Error sending email with attachment from URL:', error); - response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) }); - } + const options = { + wordwrap: 130, + }; + const textMessage = convert(message, options); + const fileBuffer = fs.readFileSync(tempFilePath); + const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); + + const data = { + from: process.env.MAILGUN_FROM_ADDRESS, + to: toAddress, + subject: subject, + text: textMessage, + html: message, + attachment: { + data: fileBuffer, + filename: attachmentFilename, + contentType: 'application/pdf', + } + }; + + const result = await client.messages.create(process.env.MAILGUN_SERVER, data); + fs.unlinkSync(tempFilePath); + + logger.info('Email with attachment from URL sent successfully'); + response.json({ success: true, result }); + } catch (error) { + logger.error('Error sending email with attachment from URL:', error); + response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) }); + } + }); }); export const accessFile = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const filePath = request.query.path as string; - if (!filePath) { - response.status(400).send('File path is required'); - return; + return corsHandler(request, response, async () => { + + try { + const filePath = request.query.path as string; + if (!filePath) { + response.status(400).send('File path is required'); + return; + } + + const expirationMs = 60 * 60 * 1000; + + const bucket = getStorage().bucket(); + const file = bucket.file(filePath); + + const [exists] = await file.exists(); + if (!exists) { + response.status(404).send('File not found'); + return; + } + + const [signedUrl] = await file.getSignedUrl({ + action: 'read', + expires: Date.now() + expirationMs, + responseDisposition: `attachment; filename="${path.basename(filePath)}"`, + }); + + response.redirect(signedUrl); + logger.info(`File access redirect for ${filePath}`); + } catch (error) { + logger.error('Error accessing file:', error); + response.status(500).send('Error accessing file'); } - - const expirationMs = 60 * 60 * 1000; - - const bucket = getStorage().bucket(); - const file = bucket.file(filePath); - - const [exists] = await file.exists(); - if (!exists) { - response.status(404).send('File not found'); - return; - } - - const [signedUrl] = await file.getSignedUrl({ - action: 'read', - expires: Date.now() + expirationMs, - responseDisposition: `attachment; filename="${path.basename(filePath)}"`, - }); - - response.redirect(signedUrl); - logger.info(`File access redirect for ${filePath}`); - } catch (error) { - logger.error('Error accessing file:', error); - response.status(500).send('Error accessing file'); - } + }); }); export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { - const mailgun = new Mailgun(formData); - const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); + return corsHandler(request, response, async () => { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const options = { - wordwrap: 130, - }; + const mailgun = new Mailgun(formData); + const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - const textMessage = convert(message, options); - mailGunClient.messages.create(process.env.MAILGUN_SERVER, { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message - }).then((res: any) => { - logger.info(res); - response.send(res); - }).catch((err: any) => { - logger.error(err); - response.send(err); + const toAddress = request.body.toAddress; + const subject = request.body.subject; + const message = request.body.message; + const options = { + wordwrap: 130, + }; + + const textMessage = convert(message, options); + mailGunClient.messages.create(process.env.MAILGUN_SERVER, { + from: process.env.MAILGUN_FROM_ADDRESS, + to: toAddress, + subject: subject, + text: textMessage, + html: message + }).then((res: any) => { + logger.info(res); + response.send(res); + }).catch((err: any) => { + logger.error(err); + response.send(err); + }); }); }); export const sendSMSMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { - const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); - const { to, body } = request.body; - client.messages - .create({ - body: body, - from: process.env.TWILIO_PHONE_NUMBER, - to: to - }) - .then((message: any) => { - logger.info('SMS sent successfully:', message.sid); - response.json({ success: true, messageId: message.sid }); - }) - .catch((error: any) => { - logger.error('Error sending SMS:', error); - response.status(500).json({ success: false, error: error.message }); - }); + return corsHandler(request, response, async () => { + + const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); + const { to, body } = request.body; + client.messages + .create({ + body: body, + from: process.env.TWILIO_PHONE_NUMBER, + to: to + }) + .then((message: any) => { + logger.info('SMS sent successfully:', message.sid); + response.json({ success: true, messageId: message.sid }); + }) + .catch((error: any) => { + logger.error('Error sending SMS:', error); + response.status(500).json({ success: false, error: error.message }); + }); + }); }); interface Invitation { @@ -180,6 +191,7 @@ export const notifyInvitation = onDocumentCreated({ document: 'notifications/{notificationId}', region: '#{SERVICES_RGN}#' }, async (event: any) => { + const invitation = event.data?.data() as Invitation; const invitationId = event.params.invitationId; @@ -343,55 +355,58 @@ export const createCashfreeOrder = onRequest({ export const verifyCashfreePayment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const orderId = request.body.order_id || request.query.order_id; + return corsHandler(request, response, async () => { - if (!orderId) { - response.status(400).json({ error: 'Order ID is required' }); - return; - } + try { + const orderId = request.body.order_id || request.query.order_id; - const clientId = process.env.CASHFREE_CLIENT_ID; - const clientSecret = process.env.CASHFREE_CLIENT_SECRET; - const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; - - const apiUrl = isTest - ? `https://sandbox.cashfree.com/pg/orders/${orderId}` - : `https://api.cashfree.com/pg/orders/${orderId}`; - - const cashfreeResponse = await axios.get( - apiUrl, - { - headers: { - 'x-api-version': '2022-09-01', - 'x-client-id': clientId, - 'x-client-secret': clientSecret - } + if (!orderId) { + response.status(400).json({ error: 'Order ID is required' }); + return; } - ); - await admin.firestore().collection('payment_orders').doc(orderId).update({ - orderStatus: cashfreeResponse.data.order_status, - paymentDetails: cashfreeResponse.data, - updatedAt: new Date() - }); + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; - if (request.headers['x-webhook-source'] === 'cashfree') { - response.status(200).send('OK'); - return; + const apiUrl = isTest + ? `https://sandbox.cashfree.com/pg/orders/${orderId}` + : `https://api.cashfree.com/pg/orders/${orderId}`; + + const cashfreeResponse = await axios.get( + apiUrl, + { + headers: { + 'x-api-version': '2022-09-01', + 'x-client-id': clientId, + 'x-client-secret': clientSecret + } + } + ); + + await admin.firestore().collection('payment_orders').doc(orderId).update({ + orderStatus: cashfreeResponse.data.order_status, + paymentDetails: cashfreeResponse.data, + updatedAt: new Date() + }); + + if (request.headers['x-webhook-source'] === 'cashfree') { + response.status(200).send('OK'); + return; + } + + response.json({ + status: cashfreeResponse.data.order_status, + paymentDetails: cashfreeResponse.data + }); + + logger.info(`Cashfree payment verified: ${orderId}`); + } catch (error: any) { + logger.error('Cashfree payment verification error:', error); + response.status(500).json({ + error: 'Failed to verify payment status', + details: error.response?.data || error.message + }); } - - response.json({ - status: cashfreeResponse.data.order_status, - paymentDetails: cashfreeResponse.data - }); - - logger.info(`Cashfree payment verified: ${orderId}`); - } catch (error: any) { - logger.error('Cashfree payment verification error:', error); - response.status(500).json({ - error: 'Failed to verify payment status', - details: error.response?.data || error.message - }); - } + }); }); From d241b09fd89741ba80c56665cd4d128d2421811e Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 21 Apr 2025 12:28:07 +0530 Subject: [PATCH 06/81] Removed env var FITLIENHOST --- functions/.env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/.env.example b/functions/.env.example index 0a823da..3f9f9c9 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -8,7 +8,6 @@ SERVICES_RGN=#{SERVICES_RGN}# CASHFREE_CLIENT_ID=#{CASHFREE_CLIENT_ID}# CASHFREE_CLIENT_SECRET=#{CASHFREE_CLIENT_SECRET}# GOOGLE_MAPS_API_KEY=#{GOOGLE_MAPS_API_KEY}# -FITLIENHOST=#{FITLIENHOST}# CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# From ba77cccd7651552f216c41449ac8988fd5733f5e Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 14:16:58 +0530 Subject: [PATCH 07/81] Fixed some errors and added the function to register client when a owner added a memmb --- .../clientRegistration/clientRegistration.ts | 96 +++++++++++++++++++ functions/src/clientRegistration/index.ts | 1 + functions/src/email/sendEmail.ts | 7 +- .../src/email/sendEmailWithAttachment.ts | 7 +- functions/src/index.ts | 1 + .../src/payments/cashfree/createOrder.ts | 8 +- .../src/payments/cashfree/verifyPayment.ts | 8 +- 7 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 functions/src/clientRegistration/clientRegistration.ts create mode 100644 functions/src/clientRegistration/index.ts diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts new file mode 100644 index 0000000..08dbe19 --- /dev/null +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -0,0 +1,96 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { getAdmin } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; + + +const corsHandler = getCorsHandler(); +const admin = getAdmin(); +export const registerClient = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (req, res) => { + return corsHandler(req, res, async () => { + try { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); + } + + const gymUser = req.body; + if (!gymUser.phoneNumber) { + return res.status(400).json({ error: 'Phone number is required' }); + } + + const isdCode = gymUser.isdCode || '+1'; + const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') + ? gymUser.phoneNumber + : `${isdCode}${gymUser.phoneNumber}`; + + let uid; + try { + const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + .catch(() => null); + + if (userRecord) { + uid = userRecord.uid; + } else { + const newUser = await admin.auth().createUser({ + phoneNumber: formattedPhoneNumber, + displayName: gymUser.name || '', + email: gymUser.email || null, + }); + uid = newUser.uid; + } + } catch (error) { + console.error('Error creating authentication user:', error); + return res.status(500).json({ + error: 'Failed to create authentication user', + details: error + }); + } + + try { + gymUser.uid = uid; + + if (gymUser.name) { + gymUser.normalizedName = gymUser.name.toLowerCase(); + } + + if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { + gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); + } + + const clientData = { + ...gymUser, + }; + + await admin.firestore().collection('client_profile').doc(uid).set(clientData); + + return res.status(201).json({ + success: true, + message: 'Client registered successfully', + clientId: uid + }); + } catch (error) { + console.error('Error creating client profile:', error); + + try { + if (!gymUser.uid) { + await admin.auth().deleteUser(uid); + } + } catch (deleteError) { + console.error('Error deleting auth user after failed profile creation:', deleteError); + } + + return res.status(500).json({ + error: 'Failed to create client profile', + details: error + }); + } + } catch (error) { + console.error('Unexpected error in client registration:', error); + return res.status(500).json({ + error: 'Internal server error', + details: error + }); + } + }); +}); diff --git a/functions/src/clientRegistration/index.ts b/functions/src/clientRegistration/index.ts new file mode 100644 index 0000000..1f2445e --- /dev/null +++ b/functions/src/clientRegistration/index.ts @@ -0,0 +1 @@ +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/email/sendEmail.ts b/functions/src/email/sendEmail.ts index d7b366f..4200b76 100644 --- a/functions/src/email/sendEmail.ts +++ b/functions/src/email/sendEmail.ts @@ -1,13 +1,14 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response) => { diff --git a/functions/src/email/sendEmailWithAttachment.ts b/functions/src/email/sendEmailWithAttachment.ts index 545112a..7c1bc74 100644 --- a/functions/src/email/sendEmailWithAttachment.ts +++ b/functions/src/email/sendEmailWithAttachment.ts @@ -4,14 +4,15 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { diff --git a/functions/src/index.ts b/functions/src/index.ts index cf82272..692183f 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,3 +5,4 @@ export { sendSMSMessage } from './sms'; export { processNotificationOnCreate } from './notifications'; export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/payments/cashfree/createOrder.ts b/functions/src/payments/cashfree/createOrder.ts index ef29814..6f57b8f 100644 --- a/functions/src/payments/cashfree/createOrder.ts +++ b/functions/src/payments/cashfree/createOrder.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); interface CashfreeOrderRequest { amount: number; customerName?: string; diff --git a/functions/src/payments/cashfree/verifyPayment.ts b/functions/src/payments/cashfree/verifyPayment.ts index 1218665..ac5f1c1 100644 --- a/functions/src/payments/cashfree/verifyPayment.ts +++ b/functions/src/payments/cashfree/verifyPayment.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const admin = getAdmin(); interface CashfreePaymentResponse { order_status: string; [key: string]: any; From 1f476a3071bb0b2f8e47526d4a77543b61284009 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 14:46:35 +0530 Subject: [PATCH 08/81] Added the validation using id token and checking if the user is a gym_owner --- .../clientRegistration/clientRegistration.ts | 153 ++++++++++-------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index 08dbe19..8ed51fb 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -1,10 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; -import { getAdmin } from "../shared/config"; import { getCorsHandler } from "../shared/middleware"; - +import { getAdmin, getLogger } from "../shared/config"; const corsHandler = getCorsHandler(); const admin = getAdmin(); +const logger = getLogger(); + export const registerClient = onRequest({ region: '#{SERVICES_RGN}#' }, async (req, res) => { @@ -13,80 +14,104 @@ export const registerClient = onRequest({ if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); } - - const gymUser = req.body; - if (!gymUser.phoneNumber) { - return res.status(400).json({ error: 'Phone number is required' }); + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' }); } - - const isdCode = gymUser.isdCode || '+1'; - const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') - ? gymUser.phoneNumber - : `${isdCode}${gymUser.phoneNumber}`; - - let uid; + const idToken = authHeader.split('Bearer ')[1]; try { - const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) - .catch(() => null); + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + const userDoc = await admin.firestore().collection('users').doc(uid).get(); - if (userRecord) { - uid = userRecord.uid; - } else { - const newUser = await admin.auth().createUser({ - phoneNumber: formattedPhoneNumber, - displayName: gymUser.name || '', - email: gymUser.email || null, + if (!userDoc.exists) { + return res.status(403).json({ error: 'Forbidden. User not found.' }); + } + + const userData = userDoc.data(); + if (!userData || !userData.roles || !userData.roles.includes('gym_owner')) { + return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' }); + } + const gymUser = req.body; + if (!gymUser.phoneNumber) { + return res.status(400).json({ error: 'Phone number is required' }); + } + + const isdCode = gymUser.isdCode || '91'; + const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') + ? gymUser.phoneNumber + : `${isdCode}${gymUser.phoneNumber}`; + + let clientUid; + try { + const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + .catch(() => null); + + if (userRecord) { + clientUid = userRecord.uid; + } else { + const newUser = await admin.auth().createUser({ + phoneNumber: formattedPhoneNumber, + displayName: gymUser.name || '', + email: gymUser.email || null, + }); + clientUid = newUser.uid; + } + } catch (error) { + logger.error('Error creating authentication user:', error); + return res.status(500).json({ + error: 'Failed to create authentication user', + details: error }); - uid = newUser.uid; } - } catch (error) { - console.error('Error creating authentication user:', error); - return res.status(500).json({ - error: 'Failed to create authentication user', - details: error - }); - } - - try { - gymUser.uid = uid; - - if (gymUser.name) { - gymUser.normalizedName = gymUser.name.toLowerCase(); - } - - if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { - gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); - } - - const clientData = { - ...gymUser, - }; - - await admin.firestore().collection('client_profile').doc(uid).set(clientData); - - return res.status(201).json({ - success: true, - message: 'Client registered successfully', - clientId: uid - }); - } catch (error) { - console.error('Error creating client profile:', error); try { - if (!gymUser.uid) { - await admin.auth().deleteUser(uid); + gymUser.uid = clientUid; + gymUser.registeredBy = uid; + + if (gymUser.name) { + gymUser.normalizedName = gymUser.name.toLowerCase(); } - } catch (deleteError) { - console.error('Error deleting auth user after failed profile creation:', deleteError); + + if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { + gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); + } + + const clientData = { + ...gymUser, + phoneNumber: formattedPhoneNumber, + }; + + await admin.firestore().collection('client_profile').doc(clientUid).set(clientData); + + return res.status(201).json({ + success: true, + message: 'Client registered successfully', + clientId: clientUid + }); + } catch (error) { + logger.error('Error creating client profile:', error); + + try { + if (!gymUser.uid) { + await admin.auth().deleteUser(clientUid); + } + } catch (deleteError) { + logger.error('Error deleting auth user after failed profile creation:', deleteError); + } + + return res.status(500).json({ + error: 'Failed to create client profile', + details: error + }); } - return res.status(500).json({ - error: 'Failed to create client profile', - details: error - }); + } catch (authError) { + logger.error('Authentication error:', authError); + return res.status(401).json({ error: 'Unauthorized. Invalid token.' }); } } catch (error) { - console.error('Unexpected error in client registration:', error); + logger.error('Unexpected error in client registration:', error); return res.status(500).json({ error: 'Internal server error', details: error From 0fba4756c74cd8c4e68f61b2eb74a0aaebde2834 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 09:19:03 +0000 Subject: [PATCH 09/81] feature/add-client (#21) Co-authored-by: Benoy Bose Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/21 Reviewed-by: Benoy Bose Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- .../clientRegistration/clientRegistration.ts | 121 ++++++++++++++++++ functions/src/clientRegistration/index.ts | 1 + functions/src/email/sendEmail.ts | 7 +- .../src/email/sendEmailWithAttachment.ts | 7 +- functions/src/index.ts | 1 + .../src/payments/cashfree/createOrder.ts | 8 +- .../src/payments/cashfree/verifyPayment.ts | 8 +- 7 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 functions/src/clientRegistration/clientRegistration.ts create mode 100644 functions/src/clientRegistration/index.ts diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts new file mode 100644 index 0000000..8ed51fb --- /dev/null +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -0,0 +1,121 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../shared/middleware"; +import { getAdmin, getLogger } from "../shared/config"; + +const corsHandler = getCorsHandler(); +const admin = getAdmin(); +const logger = getLogger(); + +export const registerClient = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (req, res) => { + return corsHandler(req, res, async () => { + try { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); + } + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' }); + } + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + const userDoc = await admin.firestore().collection('users').doc(uid).get(); + + if (!userDoc.exists) { + return res.status(403).json({ error: 'Forbidden. User not found.' }); + } + + const userData = userDoc.data(); + if (!userData || !userData.roles || !userData.roles.includes('gym_owner')) { + return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' }); + } + const gymUser = req.body; + if (!gymUser.phoneNumber) { + return res.status(400).json({ error: 'Phone number is required' }); + } + + const isdCode = gymUser.isdCode || '91'; + const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') + ? gymUser.phoneNumber + : `${isdCode}${gymUser.phoneNumber}`; + + let clientUid; + try { + const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + .catch(() => null); + + if (userRecord) { + clientUid = userRecord.uid; + } else { + const newUser = await admin.auth().createUser({ + phoneNumber: formattedPhoneNumber, + displayName: gymUser.name || '', + email: gymUser.email || null, + }); + clientUid = newUser.uid; + } + } catch (error) { + logger.error('Error creating authentication user:', error); + return res.status(500).json({ + error: 'Failed to create authentication user', + details: error + }); + } + + try { + gymUser.uid = clientUid; + gymUser.registeredBy = uid; + + if (gymUser.name) { + gymUser.normalizedName = gymUser.name.toLowerCase(); + } + + if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { + gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); + } + + const clientData = { + ...gymUser, + phoneNumber: formattedPhoneNumber, + }; + + await admin.firestore().collection('client_profile').doc(clientUid).set(clientData); + + return res.status(201).json({ + success: true, + message: 'Client registered successfully', + clientId: clientUid + }); + } catch (error) { + logger.error('Error creating client profile:', error); + + try { + if (!gymUser.uid) { + await admin.auth().deleteUser(clientUid); + } + } catch (deleteError) { + logger.error('Error deleting auth user after failed profile creation:', deleteError); + } + + return res.status(500).json({ + error: 'Failed to create client profile', + details: error + }); + } + + } catch (authError) { + logger.error('Authentication error:', authError); + return res.status(401).json({ error: 'Unauthorized. Invalid token.' }); + } + } catch (error) { + logger.error('Unexpected error in client registration:', error); + return res.status(500).json({ + error: 'Internal server error', + details: error + }); + } + }); +}); diff --git a/functions/src/clientRegistration/index.ts b/functions/src/clientRegistration/index.ts new file mode 100644 index 0000000..1f2445e --- /dev/null +++ b/functions/src/clientRegistration/index.ts @@ -0,0 +1 @@ +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/email/sendEmail.ts b/functions/src/email/sendEmail.ts index d7b366f..4200b76 100644 --- a/functions/src/email/sendEmail.ts +++ b/functions/src/email/sendEmail.ts @@ -1,13 +1,14 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response) => { diff --git a/functions/src/email/sendEmailWithAttachment.ts b/functions/src/email/sendEmailWithAttachment.ts index 545112a..7c1bc74 100644 --- a/functions/src/email/sendEmailWithAttachment.ts +++ b/functions/src/email/sendEmailWithAttachment.ts @@ -4,14 +4,15 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { diff --git a/functions/src/index.ts b/functions/src/index.ts index cf82272..692183f 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,3 +5,4 @@ export { sendSMSMessage } from './sms'; export { processNotificationOnCreate } from './notifications'; export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/payments/cashfree/createOrder.ts b/functions/src/payments/cashfree/createOrder.ts index ef29814..6f57b8f 100644 --- a/functions/src/payments/cashfree/createOrder.ts +++ b/functions/src/payments/cashfree/createOrder.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); interface CashfreeOrderRequest { amount: number; customerName?: string; diff --git a/functions/src/payments/cashfree/verifyPayment.ts b/functions/src/payments/cashfree/verifyPayment.ts index 1218665..ac5f1c1 100644 --- a/functions/src/payments/cashfree/verifyPayment.ts +++ b/functions/src/payments/cashfree/verifyPayment.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const admin = getAdmin(); interface CashfreePaymentResponse { order_status: string; [key: string]: any; From 47851da81b460775f1d1c799cc708dc826f6910a Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 16:28:03 +0530 Subject: [PATCH 10/81] changed name from corseHandler to getCorseHander in place details function --- functions/src/places/details.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/places/details.ts b/functions/src/places/details.ts index 41dbf64..4eea0a3 100644 --- a/functions/src/places/details.ts +++ b/functions/src/places/details.ts @@ -3,7 +3,7 @@ import { Request } from "firebase-functions/v2/https"; import * as express from "express"; import axios from "axios"; -const corsHandler = require('../shared/middleware').corsHandler; +const corsHandler = require('../shared/middleware').getcorsHandler(); const logger = require('../shared/config').getLogger(); From c11f0447a6ba50e172fffb77c704deeace0408b5 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 15:18:30 +0530 Subject: [PATCH 11/81] Adding AWS SES based email sending --- functions/.env.example | 6 + functions/package-lock.json | 302 ++++++++++++++-------------- functions/package.json | 2 +- functions/src/email/index.ts | 1 + functions/src/email/sendEmailSES.ts | 165 +++++++++++++++ functions/src/index.ts | 6 +- functions/src/places/details.ts | 6 +- 7 files changed, 331 insertions(+), 157 deletions(-) create mode 100644 functions/src/email/sendEmailSES.ts diff --git a/functions/.env.example b/functions/.env.example index 3f9f9c9..fa988cf 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -11,3 +11,9 @@ GOOGLE_MAPS_API_KEY=#{GOOGLE_MAPS_API_KEY}# CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# + +SES_FROM_EMAIL=#{SES_FROM_EMAIL} +SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL} +AWS_ACCESS_KEY_ID=#{AWS_ACCESS_KEY_ID}# +AWS_SECRET_ACCESS_KEY=#{AWS_SECRET_ACCESS_KEY}# +AWS_REGION=#{AWS_REGION}# diff --git a/functions/package-lock.json b/functions/package-lock.json index fc5ea89..96a716a 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -128,14 +128,14 @@ "peer": true }, "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, "peer": true, "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -145,13 +145,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", "dev": true, "peer": true, "dependencies": { - "@babel/compat-data": "^7.26.5", + "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -234,27 +234,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "peer": true, "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "peer": true, "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -503,32 +503,32 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -562,9 +562,9 @@ "peer": true }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "peer": true, "dependencies": { @@ -702,23 +702,23 @@ } }, "node_modules/@google-cloud/promisify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.1.0.tgz", - "integrity": "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", "optional": true, "engines": { - "node": ">=18" + "node": ">=14" } }, "node_modules/@google-cloud/storage": { - "version": "7.15.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.15.2.tgz", - "integrity": "sha512-+2k+mcQBb9zkaXMllf2wwR/rI07guAx+eZLWsGTDihW2lJRGfiqB7xu1r7/s4uvSP/T+nAumvzT5TTscwHKJ9A==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", + "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", "optional": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", @@ -746,9 +746,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.0.tgz", - "integrity": "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz", + "integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==", "optional": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -759,9 +759,9 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", @@ -777,9 +777,9 @@ } }, "node_modules/@grpc/proto-loader/node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "optional": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1165,30 +1165,30 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.68.tgz", - "integrity": "sha512-LQESrePLEBLvhuFkXx9jjBXRC2ClYsO5mqQ1m/puth5z9SOuM3N/B3vDuqnC3RJFktDktyK9khGvo7dTkqO9uQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.69.tgz", + "integrity": "sha512-ydvNeJMRm+l3T14yCoUKqjYQiEdXDq1isznI93LEBGYssXKfSaLNLHOkeM4z9Fnw9Pkt2EKOCAtW9cS4b00Zcg==", "optional": true, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.68", - "@napi-rs/canvas-darwin-arm64": "0.1.68", - "@napi-rs/canvas-darwin-x64": "0.1.68", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.68", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.68", - "@napi-rs/canvas-linux-arm64-musl": "0.1.68", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.68", - "@napi-rs/canvas-linux-x64-gnu": "0.1.68", - "@napi-rs/canvas-linux-x64-musl": "0.1.68", - "@napi-rs/canvas-win32-x64-msvc": "0.1.68" + "@napi-rs/canvas-android-arm64": "0.1.69", + "@napi-rs/canvas-darwin-arm64": "0.1.69", + "@napi-rs/canvas-darwin-x64": "0.1.69", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.69", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.69", + "@napi-rs/canvas-linux-arm64-musl": "0.1.69", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.69", + "@napi-rs/canvas-linux-x64-gnu": "0.1.69", + "@napi-rs/canvas-linux-x64-musl": "0.1.69", + "@napi-rs/canvas-win32-x64-msvc": "0.1.69" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.68.tgz", - "integrity": "sha512-h1KcSR4LKLfRfzeBH65xMxbWOGa1OtMFQbCMVlxPCkN1Zr+2gK+70pXO5ktojIYcUrP6KDcOwoc8clho5ccM/w==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.69.tgz", + "integrity": "sha512-4icWTByY8zPvM9SelfQKf3I6kwXw0aI5drBOVrwfER5kjwXJd78FPSDSZkxDHjvIo9Q86ljl18Yr963ehA4sHQ==", "cpu": [ "arm64" ], @@ -1201,9 +1201,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.68.tgz", - "integrity": "sha512-/VURlrAD4gDoxW1GT/b0nP3fRz/fhxmHI/xznTq2FTwkQLPOlLkDLCvTmQ7v6LtGKdc2Ed6rvYpRan+JXThInQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.69.tgz", + "integrity": "sha512-HOanhhYlHdukA+unjelT4Dg3ta7e820x87/AG2dKUMsUzH19jaeZs9bcYjzEy2vYi/dFWKz7cSv2yaIOudB8Yg==", "cpu": [ "arm64" ], @@ -1216,9 +1216,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.68.tgz", - "integrity": "sha512-tEpvGR6vCLTo1Tx9wmDnoOKROpw57wiCWwCpDOuVlj/7rqEJOUYr9ixW4aRJgmeGBrZHgevI0EURys2ER6whmg==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.69.tgz", + "integrity": "sha512-SIp7WfhxAPnSVK9bkFfJp+84rbATCIq9jMUzDwpCLhQ+v+OqtXe4pggX1oeV+62/HK6BT1t18qRmJfyqwJ9f3g==", "cpu": [ "x64" ], @@ -1231,9 +1231,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.68.tgz", - "integrity": "sha512-U9xbJsumPOiAYeAFZMlHf62b9dGs2HJ6Q5xt7xTB0uEyPeurwhgYBWGgabdsEidyj38YuzI/c3LGBbSQB3vagw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.69.tgz", + "integrity": "sha512-Ls+KujCp6TGpkuMVFvrlx+CxtL+casdkrprFjqIuOAnB30Mct6bCEr+I83Tu29s3nNq4EzIGjdmA3fFAZG/Dtw==", "cpu": [ "arm" ], @@ -1246,9 +1246,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.68.tgz", - "integrity": "sha512-KFkn8wEm3mPnWD4l8+OUUkxylSJuN5q9PnJRZJgv15RtCA1bgxIwTkBhI/+xuyVMcHqON9sXq7cDkEJtHm35dg==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.69.tgz", + "integrity": "sha512-m8VcGmeSBNRbHZBd1srvdM1aq/ScS2y8KqGqmCCEgJlytYK4jdULzAo2K/BPKE1v3xvn8oUPZDLI/NBJbJkEoA==", "cpu": [ "arm64" ], @@ -1261,9 +1261,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.68.tgz", - "integrity": "sha512-IQzts91rCdOALXBWQxLZRCEDrfFTGDtNRJMNu+2SKZ1uT8cmPQkPwVk5rycvFpvgAcmiFiOSCp1aRrlfU8KPpQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.69.tgz", + "integrity": "sha512-a3xjNRIeK2m2ZORGv2moBvv3vbkaFZG1QKMeiEv/BKij+rkztuEhTJGMar+buICFgS0fLgphXXsKNkUSJb7eRQ==", "cpu": [ "arm64" ], @@ -1276,9 +1276,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.68.tgz", - "integrity": "sha512-e9AS5UttoIKqXSmBzKZdd3NErSVyOEYzJfNOCGtafGk1//gibTwQXGlSXmAKuErqMp09pyk9aqQRSYzm1AQfBw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.69.tgz", + "integrity": "sha512-pClUoJF5wdC9AvD0mc15G9JffL1Q85nuH1rLSQPRkGmGmQOtRjw5E9xNbanz7oFUiPbjH7xcAXUjVAcf7tdgPQ==", "cpu": [ "riscv64" ], @@ -1291,9 +1291,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.68.tgz", - "integrity": "sha512-Pa/I36VE3j57I3Obhrr+J48KGFfkZk2cJN/2NmW/vCgmoF7kCP6aTVq5n+cGdGWLd/cN9CJ9JvNwEoMRDghu0g==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.69.tgz", + "integrity": "sha512-96X3bFAmzemfw84Ts6Jg/omL86uuynvK06MWGR/mp3JYNumY9RXofA14eF/kJIYelbYFWXcwpbcBR71lJ6G/YQ==", "cpu": [ "x64" ], @@ -1306,9 +1306,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.68.tgz", - "integrity": "sha512-9c6rkc5195wNxuUHJdf4/mmnq433OQey9TNvQ9LspJazvHbfSkTij8wtKjASVQsJyPDva4fkWOeV/OQ7cLw0GQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.69.tgz", + "integrity": "sha512-2QTsEFO72Kwkj53W9hc5y1FAUvdGx0V+pjJB+9oQF6Ys9+y989GyPIl5wZDzeh8nIJW6koZZ1eFa8pD+pA5BFQ==", "cpu": [ "x64" ], @@ -1321,9 +1321,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.68.tgz", - "integrity": "sha512-Fc5Dez23u0FoSATurT6/w1oMytiRnKWEinHivdMvXpge6nG4YvhrASrtqMk8dGJMVQpHr8QJYF45rOrx2YU2Aw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.69.tgz", + "integrity": "sha512-Q4YA8kVnKarApBVLu7F8icGlIfSll5Glswo5hY6gPS4Is2dCI8+ig9OeDM8RlwYevUIxKq8lZBypN8Q1iLAQ7w==", "cpu": [ "x64" ], @@ -1461,9 +1461,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "peer": true, "dependencies": { @@ -1482,9 +1482,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, "peer": true, "dependencies": { @@ -1622,11 +1622,11 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-fetch": { @@ -1851,9 +1851,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2009,9 +2009,9 @@ "optional": true }, "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", "optional": true, "engines": { "node": "*" @@ -2175,9 +2175,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001704", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", - "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "dev": true, "funding": [ { @@ -2572,9 +2572,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.116", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.116.tgz", - "integrity": "sha512-mufxTCJzLBQVvSdZzX1s5YAuXsN1M4tTyYxOOL1TcSKtIzQ9rjIrm7yFK80rN5dwGTePgdoABDSHpuVtRQh0Zw==", + "version": "1.5.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz", + "integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==", "dev": true, "peer": true }, @@ -3259,9 +3259,9 @@ } }, "node_modules/google-gax": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", - "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.0.tgz", + "integrity": "sha512-zKKLeLfcYBVOzzM48Brtn4EQkKcTli9w6c1ilzFK2NbJvcd4ATD8/XqFExImvE/W5IwMlKKwa5qqVufji3ioNQ==", "optional": true, "dependencies": { "@grpc/grpc-js": "^1.10.9", @@ -3387,9 +3387,9 @@ } }, "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "funding": [ { "type": "github", @@ -3458,9 +3458,9 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", - "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==" + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==" }, "node_modules/http-proxy-agent": { "version": "5.0.0", @@ -4538,14 +4538,14 @@ } }, "node_modules/jwks-rsa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", - "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", "dependencies": { - "@types/express": "^4.17.17", - "@types/jsonwebtoken": "^9.0.2", + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", - "jose": "^4.14.6", + "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" }, @@ -5172,9 +5172,9 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/pdfjs-dist": { - "version": "5.0.375", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.0.375.tgz", - "integrity": "sha512-QDRlEVldf/cX88CElGAyRhjqNOO69kmB3HZlalDAfqJ/IvmqJNkipomhBZy4cWATfLVlkQTXE3H4yFvMF2uPsg==", + "version": "5.1.91", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.1.91.tgz", + "integrity": "sha512-qSIADdagooJB4wWCBnrBJjRvASevmxL0BwafvOuKJG5uTQdYoFBrhrRYnucKNiSc9qS6JIk0hC5y1yktFljXkA==", "engines": { "node": ">=20" }, @@ -5211,9 +5211,9 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "peer": true, "engines": { @@ -5288,9 +5288,9 @@ } }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.0.tgz", + "integrity": "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -5311,9 +5311,9 @@ } }, "node_modules/protobufjs/node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -6050,9 +6050,9 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/twilio": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.5.0.tgz", - "integrity": "sha512-iyI9kP3ZSrffUb9XApRNd35MMsV4Yp3OZM3Ewg7JNt5FvscsZZel/vCaxTlJ9D5kss0FKdeeu2c+GnaUK6Z1VA==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.5.2.tgz", + "integrity": "sha512-yrRh6immcL5xEVX7VmHsl3vU01x/fmqxf38kvxMrrtvEtAkYARYOPor9lt5T7964zC7l31k5sTrnLJmd2jjeOA==", "dependencies": { "axios": "^1.7.8", "dayjs": "^1.11.9", @@ -6146,9 +6146,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -6159,9 +6159,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unpipe": { "version": "1.0.0", diff --git a/functions/package.json b/functions/package.json index f593fbc..79a4d7d 100644 --- a/functions/package.json +++ b/functions/package.json @@ -36,4 +36,4 @@ "typescript": "^5.8.2" }, "private": true -} +} \ No newline at end of file diff --git a/functions/src/email/index.ts b/functions/src/email/index.ts index 13a1701..8a5ad52 100644 --- a/functions/src/email/index.ts +++ b/functions/src/email/index.ts @@ -1,2 +1,3 @@ export { sendEmailMessage } from './sendEmail'; export { sendEmailWithAttachment } from './sendEmailWithAttachment'; +export { sendEmailSES } from './sendEmailSES'; diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts new file mode 100644 index 0000000..a99c3b1 --- /dev/null +++ b/functions/src/email/sendEmailSES.ts @@ -0,0 +1,165 @@ +import { getLogger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { SESClient } from "@aws-sdk/client-ses"; +import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; +import { HttpsError } from "firebase-functions/v2/https"; +import * as mime from 'mime-types'; + +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +interface EmailRequest { + to: string | string[]; + subject: string; + html: string; + text?: string; + from: string; + replyTo?: string; + attachments?: Attachment[]; +} + +interface Attachment { + filename: string; + content: string | Buffer; // Base64 encoded string or Buffer + contentType?: string; +} + +const stripHtml = (html: string): string => { + if (!html) return ''; + return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); +} + +async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { + const ses = new SESClient({ + region: 'ap-south-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' + } + }); + + const command = new SendEmailCommand({ + Source: data.from, + Destination: { ToAddresses: recipients }, + Message: { + Subject: { Data: data.subject }, + Body: { + Html: { Data: data.html }, + Text: { Data: data.text || stripHtml(data.html) } + } + }, + ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined, + }); + + const result = await ses.send(command); + return { messageId: result.MessageId }; +} + +async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { + const ses = new SESClient({ + region: 'ap-south-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' + } + }); + + const boundary = `boundary_${Math.random().toString(16).substr(2)}`; + let rawMessage = `From: ${data.from}\n`; + rawMessage += `To: ${recipients.join(', ')}\n`; + rawMessage += `Subject: ${data.subject}\n`; + rawMessage += `MIME-Version: 1.0\n`; + rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; + + // Add email body (multipart/alternative) + rawMessage += `--${boundary}\n`; + rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; + + // Text part + if (data.text) { + rawMessage += `--alt_${boundary}\n`; + rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; + rawMessage += `${data.text}\n\n`; + } + + // HTML part + rawMessage += `--alt_${boundary}\n`; + rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; + rawMessage += `${data.html}\n\n`; + + // Close alternative part + rawMessage += `--alt_${boundary}--\n\n`; + + // Add attachments + for (const attachment of data.attachments || []) { + const contentType = attachment.contentType || + mime.lookup(attachment.filename) || + 'application/octet-stream'; + + rawMessage += `--${boundary}\n`; + rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`; + rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; + rawMessage += `Content-Transfer-Encoding: base64\n\n`; + + const contentBuffer = typeof attachment.content === 'string' + ? Buffer.from(attachment.content, 'base64') + : attachment.content; + + rawMessage += contentBuffer.toString('base64') + '\n\n'; + } + + // Close message + rawMessage += `--${boundary}--`; + + const command = new SendRawEmailCommand({ + RawMessage: { Data: Buffer.from(rawMessage) } + }); + + const result = await ses.send(command); + return { messageId: result.MessageId }; +} + +export const sendEmailSES = onRequest({ + region: 'asia-south1' +}, (request: Request, response) => { + return corsHandler(request, response, async () => { + const toAddress = request.body.toAddress; + const subject = request.body.subject; + const message = request.body.message; + const data: EmailRequest = { + to: toAddress, + subject: subject, + html: message, + text: stripHtml(message), + from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', + replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', + attachments: request.body.attachments as Attachment[] || [] + }; + try { + if (!data.to || !data.subject || !data.html || !data.from) { + throw new HttpsError( + 'invalid-argument', + 'Missing required email fields' + ); + } + logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); + const recipients = Array.isArray(data.to) ? data.to : [data.to]; + if (data.attachments && data.attachments.length > 0) { + const messageResult = await sendEmailWithAttachments(data, recipients); + response.status(200).json(messageResult); + } else { + const messageResult = await sendSimpleEmail(data, recipients); + response.status(200).json(messageResult); + } + } catch (e) { + logger.error(`Error while sending E-mail. Error: ${e}`); + console.error(`Error while sending E-mail. Error: ${e}`); + response.status(500).json({ + success: false, + error: 'Error while sending E-mail' + }); + } + }); +}); \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 692183f..a0fb365 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,8 +1,8 @@ export * from './shared/config'; -export { sendEmailMessage, sendEmailWithAttachment } from './email'; -export { accessFile } from './storage'; +export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email'; export { sendSMSMessage } from './sms'; +export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; -export { registerClient } from './clientRegistration'; \ No newline at end of file +export { registerClient } from './clientRegistration'; diff --git a/functions/src/places/details.ts b/functions/src/places/details.ts index 4eea0a3..7ad9f43 100644 --- a/functions/src/places/details.ts +++ b/functions/src/places/details.ts @@ -3,8 +3,10 @@ import { Request } from "firebase-functions/v2/https"; import * as express from "express"; import axios from "axios"; -const corsHandler = require('../shared/middleware').getcorsHandler(); -const logger = require('../shared/config').getLogger(); +const { getCorsHandler } = require('../shared/middleware'); +const corsHandler = getCorsHandler(); +const { getLogger } = require('../shared/config'); +const logger = getLogger(); export const getPlaceDetails = onRequest({ From bb1a33daa47df549e371995b16e1ec1795580c31 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 16:53:08 +0530 Subject: [PATCH 12/81] Adding actions --- .gitea/workflows/deploy-dev.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .gitea/workflows/deploy-dev.yaml diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml new file mode 100644 index 0000000..91ba3db --- /dev/null +++ b/.gitea/workflows/deploy-dev.yaml @@ -0,0 +1,15 @@ +name: Deploy FitLien services to Dev + +on: + push: + branches: + - dev +jobs: + deploy: + name: Deploy to Dev + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 From 59a9932ca816cdfe3fe72e80866cbc993fcfbea1 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 17:40:47 +0530 Subject: [PATCH 13/81] Added more steps --- .gitea/workflows/deploy-dev.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 91ba3db..4c55a2c 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -13,3 +13,14 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 22 + + - name: Clean install + run: npm clean-install + + - name: Copy .env.example to .env + run: cp .env.example .env From b6c72adfb89782b83183426242e3b63e5b5019f8 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 17:44:48 +0530 Subject: [PATCH 14/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 4c55a2c..f2b0d08 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -23,4 +23,4 @@ jobs: run: npm clean-install - name: Copy .env.example to .env - run: cp .env.example .env + run: cp /functions/.env.example /functions/.env From 3d8e1c8db0b72180eb0fcf94e4ff1ca09069bc41 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 17:49:39 +0530 Subject: [PATCH 15/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index f2b0d08..b3747f4 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -23,4 +23,4 @@ jobs: run: npm clean-install - name: Copy .env.example to .env - run: cp /functions/.env.example /functions/.env + run: cp functions/.env.example functions/.env From b40201cf938f2e5f940d75f0b87ab915ddbc5ef4 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:17:21 +0530 Subject: [PATCH 16/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index b3747f4..b66787e 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -24,3 +24,8 @@ jobs: - name: Copy .env.example to .env run: cp functions/.env.example functions/.env + + - name: Replace MAILGUN_API_KEY in .env + run: | + sed -i "s/#{MAILGUN_API_KEY}#/${MAILGUN_API_KEY}/" functions/.env + sed -i "s/#{MAILGUN_SERVER}#/${MAILGUN_SERVER}/" functions/.env From 2d3c967037a378d646145557c831c80022321f9a Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:25:35 +0530 Subject: [PATCH 17/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index b66787e..a7b5378 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -29,3 +29,5 @@ jobs: run: | sed -i "s/#{MAILGUN_API_KEY}#/${MAILGUN_API_KEY}/" functions/.env sed -i "s/#{MAILGUN_SERVER}#/${MAILGUN_SERVER}/" functions/.env + sed -i "s/#{MAILGUN_FROM_ADDRESS}#/${MAILGUN_FROM_ADDRESS}/" functions/.env + cat functions/.env From e7d6fd54e8f2c9cc23bdbd1d0e6cd313a50cad5a Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:28:04 +0530 Subject: [PATCH 18/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index a7b5378..1574248 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -27,7 +27,7 @@ jobs: - name: Replace MAILGUN_API_KEY in .env run: | - sed -i "s/#{MAILGUN_API_KEY}#/${MAILGUN_API_KEY}/" functions/.env - sed -i "s/#{MAILGUN_SERVER}#/${MAILGUN_SERVER}/" functions/.env - sed -i "s/#{MAILGUN_FROM_ADDRESS}#/${MAILGUN_FROM_ADDRESS}/" functions/.env + sed -i "s/#{MAILGUN_API_KEY}#/${{ secrets.MAILGUN_API_KEY }}/" functions/.env + sed -i "s/#{MAILGUN_SERVER}#/${{ secrets.MAILGUN_SERVER }}/" functions/.env + sed -i "s/#{MAILGUN_FROM_ADDRESS}#/${{ secrets.MAILGUN_FROM_ADDRESS }}/" functions/.env cat functions/.env From 4e1724b1ecb1b5ab524f685bc5c207101a953dd3 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:44:42 +0530 Subject: [PATCH 19/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 1574248..0517196 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -30,4 +30,14 @@ jobs: sed -i "s/#{MAILGUN_API_KEY}#/${{ secrets.MAILGUN_API_KEY }}/" functions/.env sed -i "s/#{MAILGUN_SERVER}#/${{ secrets.MAILGUN_SERVER }}/" functions/.env sed -i "s/#{MAILGUN_FROM_ADDRESS}#/${{ secrets.MAILGUN_FROM_ADDRESS }}/" functions/.env + sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env + sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env + sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env + sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env + sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env + sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env + sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env + sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env + sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env + sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env cat functions/.env From 853dd0e1e05bde411b6b41a3977c22630e66ebac Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:46:04 +0530 Subject: [PATCH 20/81] Update .env.example --- functions/.env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/.env.example b/functions/.env.example index fa988cf..397672a 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -12,8 +12,8 @@ CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# -SES_FROM_EMAIL=#{SES_FROM_EMAIL} -SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL} +SES_FROM_EMAIL=#{SES_FROM_EMAIL}# +SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL}# AWS_ACCESS_KEY_ID=#{AWS_ACCESS_KEY_ID}# AWS_SECRET_ACCESS_KEY=#{AWS_SECRET_ACCESS_KEY}# AWS_REGION=#{AWS_REGION}# From dcee3e6af923642a386e7b2d3c5d95c53d16a5dd Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:51:49 +0530 Subject: [PATCH 21/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 0517196..8fbe0fc 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -25,7 +25,7 @@ jobs: - name: Copy .env.example to .env run: cp functions/.env.example functions/.env - - name: Replace MAILGUN_API_KEY in .env + - name: Replace variables in .env run: | sed -i "s/#{MAILGUN_API_KEY}#/${{ secrets.MAILGUN_API_KEY }}/" functions/.env sed -i "s/#{MAILGUN_SERVER}#/${{ secrets.MAILGUN_SERVER }}/" functions/.env @@ -41,3 +41,5 @@ jobs: sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env cat functions/.env + - name: "Replace #{SERVICES_RGN}# in all .ts files" + run: find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + From da1430ebc3e305cd0e0b819fb9e3c7a4d75c6b73 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:54:56 +0530 Subject: [PATCH 22/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 8fbe0fc..c59b209 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,4 +42,6 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" - run: find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + + run: | + find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + + cat functions/src/email/sendEmail.ts From 71d9c2e67075ab55cb00470df8c1662bbe5a5275 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 18:57:45 +0530 Subject: [PATCH 23/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index c59b209..79e143a 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -45,3 +45,11 @@ jobs: run: | find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + cat functions/src/email/sendEmail.ts + - name: Build + run: | + npm install -g typescript + cd functions + npm install + npx tsc + cd .. + ls -la From ade7cfe30a699021197e2ca6c828b4feef69dae2 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 19:00:42 +0530 Subject: [PATCH 24/81] Adding ws-sdk --- functions/package-lock.json | 335 +++++++++++++++++++++++++++++++++++- functions/package.json | 3 +- 2 files changed, 335 insertions(+), 3 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 96a716a..0334e5a 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@types/node-fetch": "^2.6.12", + "aws-sdk": "^2.1692.0", "axios": "^1.8.4", "cors": "^2.8.5", "firebase-admin": "^12.6.0", @@ -1850,6 +1851,49 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", @@ -2005,8 +2049,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/bignumber.js": { "version": "9.3.0", @@ -2107,6 +2150,16 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2127,6 +2180,23 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2437,6 +2507,22 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2731,6 +2817,14 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3019,6 +3113,20 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -3350,6 +3458,17 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3568,6 +3687,11 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3623,6 +3747,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3630,6 +3769,17 @@ "dev": true, "peer": true }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3665,6 +3815,23 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3675,6 +3842,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3687,6 +3871,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4399,6 +4602,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -5233,6 +5444,14 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -5332,6 +5551,11 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -5363,6 +5587,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5511,11 +5744,32 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "node_modules/scmp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", @@ -5603,6 +5857,22 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6202,11 +6472,32 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6317,6 +6608,26 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -6354,6 +6665,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", diff --git a/functions/package.json b/functions/package.json index 79a4d7d..e6ea1b1 100644 --- a/functions/package.json +++ b/functions/package.json @@ -16,6 +16,7 @@ "main": "lib/index.js", "dependencies": { "@types/node-fetch": "^2.6.12", + "aws-sdk": "^2.1692.0", "axios": "^1.8.4", "cors": "^2.8.5", "firebase-admin": "^12.6.0", @@ -36,4 +37,4 @@ "typescript": "^5.8.2" }, "private": true -} \ No newline at end of file +} From 6e15bd303412f22ceeccb6861d4a6a471c43c11e Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 19:05:37 +0530 Subject: [PATCH 25/81] Adding @aws-sdk/client-ses --- functions/package-lock.json | 1161 ++++++++++++++++++++++++++++++++++- functions/package.json | 2 + 2 files changed, 1161 insertions(+), 2 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 0334e5a..d37a055 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,6 +8,7 @@ "name": "functions", "version": "0.0.0", "dependencies": { + "@aws-sdk/client-ses": "^3.796.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -25,6 +26,7 @@ }, "devDependencies": { "@types/long": "^5.0.0", + "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" @@ -47,6 +49,600 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-ses": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.796.0.tgz", + "integrity": "sha512-gg1z0uXtm6v3wCMrGM5eCh69aWT+VOzYvNdbSMA5EPLfW9qsOCH1Zz/q9XzUZjWN25OnGvfm1LzypzQgRypSeQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.796.0", + "@aws-sdk/credential-provider-node": "3.796.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.787.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.796.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.796.0.tgz", + "integrity": "sha512-EJExg8mbwqP0VG+RNFV4ZPuUo7QsDsUfTnuFQY51V8iXrbOdV+PDLRr4psXj2fxvrLxc9AlGUMNqd/j4VZtQzA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.796.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.787.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.796.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.796.0.tgz", + "integrity": "sha512-tH8Sp7lCxISVoLnkyv4AouuXs2CDlMhTuesWa0lq2NX1f+DXsMwSBtN37ttZdpFMw3F8mWdsJt27X9h2Oq868A==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/core": "^3.2.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core/node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.796.0.tgz", + "integrity": "sha512-kQzGKm4IOYYO6vUrai2JocNwhJm4Aml2BsAV+tBhFhhkutE7khf9PUucoVjB78b0J48nF+kdSacqzY+gB81/Uw==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.796.0.tgz", + "integrity": "sha512-wWOT6VAHIKOuHdKFGm1iyKvx7f6+Kc/YTzFWJPuT+l+CPlXR6ylP1UMIDsHHLKpMzsrh3CH77QDsjkhQrnKkfg==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.796.0.tgz", + "integrity": "sha512-qGWBDn9aO8avFfYU7daps7Sy6OglF1x0q0w48slt0KMXbHd2/LvKVIiYwyofYCXed0yzcEOF2IYm9FjXdcn+ug==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/credential-provider-env": "3.796.0", + "@aws-sdk/credential-provider-http": "3.796.0", + "@aws-sdk/credential-provider-process": "3.796.0", + "@aws-sdk/credential-provider-sso": "3.796.0", + "@aws-sdk/credential-provider-web-identity": "3.796.0", + "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.796.0.tgz", + "integrity": "sha512-WeNK7OWPrsOvhO3DAgpUO0FtmVghMaZ/IpPJHJ4Y0nBIsWOBXLrbZ2Y1mdT8N2bGGUaM91tJaV8Yf8COc3gvmA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.796.0", + "@aws-sdk/credential-provider-http": "3.796.0", + "@aws-sdk/credential-provider-ini": "3.796.0", + "@aws-sdk/credential-provider-process": "3.796.0", + "@aws-sdk/credential-provider-sso": "3.796.0", + "@aws-sdk/credential-provider-web-identity": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.796.0.tgz", + "integrity": "sha512-r4e8/4AdKn/qQbRVocW7oXkpoiuXdTv0qty8AASNLnbQnT1vjD1bvmP6kp4fbHPWgwY8I9h0Dqjp49uy9Bqyuw==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.796.0.tgz", + "integrity": "sha512-RUYsQ1t6UdzkpZ7pocUt1l/9l9GCYCaopIhv0DU6CipA8rkWtoweKsLHKdv+8wE4p6gqDfDIHGam1ivswiCIzg==", + "dependencies": { + "@aws-sdk/client-sso": "3.796.0", + "@aws-sdk/core": "3.796.0", + "@aws-sdk/token-providers": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.796.0.tgz", + "integrity": "sha512-dpmFJT4IyjT09vruvMu/rWQQjVreqdxAe8pLPpGhoeKyA1O6+PS73b+VNXKvD31rQT8e4g6dVpA6KMxNW63aag==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.775.0.tgz", + "integrity": "sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.775.0.tgz", + "integrity": "sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.775.0.tgz", + "integrity": "sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.796.0.tgz", + "integrity": "sha512-IeNg+3jNWT37J45opi5Jx89hGF0lOnZjiNwlMp3rKq7PlOqy8kWq5J1Gxk0W3tIkPpuf68CtBs/QFrRXWOjsZw==", + "dependencies": { + "@aws-sdk/core": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.787.0", + "@smithy/core": "^3.2.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.796.0.tgz", + "integrity": "sha512-jJ8a0ldWtXh/ice7nldUjTqja7KYlSYk1pwfIIvJLIqEn2SvQHK/pyCINTmmOmFAWXMKBQBeWUMxo1pPYNytzQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.796.0", + "@aws-sdk/middleware-host-header": "3.775.0", + "@aws-sdk/middleware-logger": "3.775.0", + "@aws-sdk/middleware-recursion-detection": "3.775.0", + "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/region-config-resolver": "3.775.0", + "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-endpoints": "3.787.0", + "@aws-sdk/util-user-agent-browser": "3.775.0", + "@aws-sdk/util-user-agent-node": "3.796.0", + "@smithy/config-resolver": "^4.1.0", + "@smithy/core": "^3.2.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.8", + "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-endpoints": "^3.0.2", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.775.0.tgz", + "integrity": "sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.796.0.tgz", + "integrity": "sha512-Sxr/EqJBxOwLsXHv8C91N/Aao8Rgjn5bcpzplrTZ7wrfDrzqQfSCvjh7apCxdLYMKPBV+an75blCAd7JD4/bAg==", + "dependencies": { + "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.775.0.tgz", + "integrity": "sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.787.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.787.0.tgz", + "integrity": "sha512-fd3zkiOkwnbdbN0Xp9TsP5SWrmv0SpT70YEdbb8wAj2DWQwiCmFszaSs+YCvhoCdmlR3Wl9Spu0pGpSAGKeYvQ==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "@smithy/util-endpoints": "^3.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", + "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.775.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.775.0.tgz", + "integrity": "sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A==", + "dependencies": { + "@aws-sdk/types": "3.775.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.796.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.796.0.tgz", + "integrity": "sha512-9fQpNcHgVFitf1tbTT8V1xGRoRHSmOAWjrhevo6Tc0WoINMAKz+4JNqfVGWRE5Tmtpq0oHKo1RmvxXQQtJYciA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/types": "3.775.0", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1438,6 +2034,557 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.0.tgz", + "integrity": "sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.2.0.tgz", + "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", + "dependencies": { + "@smithy/middleware-serde": "^4.0.3", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-stream": "^4.2.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.2.tgz", + "integrity": "sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", + "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.2.tgz", + "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz", + "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz", + "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz", + "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", + "dependencies": { + "@smithy/core": "^3.2.0", + "@smithy/middleware-serde": "^4.0.3", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz", + "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/service-error-classification": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.2", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.3.tgz", + "integrity": "sha512-rfgDVrgLEVMmMn0BI8O+8OVr6vXzjV7HZj57l0QxslhzbvVfikZbVfBVthjLHqib4BW44QhcIgJpvebHlRaC9A==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.2.tgz", + "integrity": "sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.2.tgz", + "integrity": "sha512-WgCkILRZfJwJ4Da92a6t3ozN/zcvYyJGUTmfGbgS/FkCcoCjl7G4FJaCDN1ySdvLvemnQeo25FdkyMSTSwulsw==", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", + "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", + "dependencies": { + "@smithy/abort-controller": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.2.tgz", + "integrity": "sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", + "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.2.tgz", + "integrity": "sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.2.tgz", + "integrity": "sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ==", + "dependencies": { + "@smithy/types": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz", + "integrity": "sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.0.tgz", + "integrity": "sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.0.tgz", + "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", + "dependencies": { + "@smithy/core": "^3.2.0", + "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", + "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.2.tgz", + "integrity": "sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==", + "dependencies": { + "@smithy/querystring-parser": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz", + "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz", + "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", + "dependencies": { + "@smithy/config-resolver": "^4.1.0", + "@smithy/credential-provider-imds": "^4.0.2", + "@smithy/node-config-provider": "^4.0.2", + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.2.tgz", + "integrity": "sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.2.tgz", + "integrity": "sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.2.tgz", + "integrity": "sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg==", + "dependencies": { + "@smithy/service-error-classification": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.0.tgz", + "integrity": "sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.3.tgz", + "integrity": "sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1617,6 +2764,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2083,6 +3236,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6148,8 +7306,7 @@ "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ], - "optional": true + ] }, "node_modules/stubs": { "version": "3.0.0", diff --git a/functions/package.json b/functions/package.json index e6ea1b1..4d2e4a9 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,6 +15,7 @@ }, "main": "lib/index.js", "dependencies": { + "@aws-sdk/client-ses": "^3.796.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -32,6 +33,7 @@ }, "devDependencies": { "@types/long": "^5.0.0", + "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" From 72ad257e07ac12460c2e022449da56d5271624b6 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Fri, 25 Apr 2025 19:17:01 +0530 Subject: [PATCH 26/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 79e143a..2df9af5 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -53,3 +53,9 @@ jobs: npx tsc cd .. ls -la + + - name: Deploy + run: | + curl -sL firebase.tools | upgrade=true bash + firebase use --token ${{ secrets.FIREBASE_TOKEN }} debug + firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive From 22fb060cb9cff57655737d822b297198ae9e2d38 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 2 May 2025 19:04:21 +0530 Subject: [PATCH 27/81] phonepe --- firebase.json | 6 +- functions/package-lock.json | 246 +++++++++--------- functions/package.json | 2 +- functions/src/index.ts | 2 +- functions/src/payments/index.ts | 1 + functions/src/payments/phonepe/checkStatus.ts | 139 ++++++++++ .../payments/phonepe/createPhonepeOrder.ts | 133 ++++++++++ functions/src/payments/phonepe/index.ts | 5 + functions/src/payments/phonepe/paymentLink.ts | 183 +++++++++++++ .../src/payments/phonepe/paymentLinkStatus.ts | 122 +++++++++ functions/src/payments/phonepe/webhook.ts | 102 ++++++++ functions/tsconfig.json | 4 +- package.json | 3 - 13 files changed, 816 insertions(+), 132 deletions(-) create mode 100644 functions/src/payments/phonepe/checkStatus.ts create mode 100644 functions/src/payments/phonepe/createPhonepeOrder.ts create mode 100644 functions/src/payments/phonepe/index.ts create mode 100644 functions/src/payments/phonepe/paymentLink.ts create mode 100644 functions/src/payments/phonepe/paymentLinkStatus.ts create mode 100644 functions/src/payments/phonepe/webhook.ts diff --git a/firebase.json b/firebase.json index dd93bf2..0eb205c 100644 --- a/firebase.json +++ b/firebase.json @@ -22,17 +22,17 @@ }, "emulators": { "functions": { - "port": 5001 + "port": 5005 }, "firestore": { - "port": 8079 + "port": 8081 }, "storage": { "port": 9199 }, "ui": { "enabled": true, - "port": 4000 + "port": 4008 }, "auth": { "port": 9099 diff --git a/functions/package-lock.json b/functions/package-lock.json index d37a055..28053b5 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,7 +8,7 @@ "name": "functions", "version": "0.0.0", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -165,44 +165,44 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.796.0.tgz", - "integrity": "sha512-gg1z0uXtm6v3wCMrGM5eCh69aWT+VOzYvNdbSMA5EPLfW9qsOCH1Zz/q9XzUZjWN25OnGvfm1LzypzQgRypSeQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.798.0.tgz", + "integrity": "sha512-s12ntssTgau7ySHoOXVAliVM1a1JyjsFYwauKh/7gCriJ+lIBlo25BFgCjZAHxxBn2FfCete6BVQFEbN/Ishkg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-node": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-node": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -215,43 +215,43 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.796.0.tgz", - "integrity": "sha512-EJExg8mbwqP0VG+RNFV4ZPuUo7QsDsUfTnuFQY51V8iXrbOdV+PDLRr4psXj2fxvrLxc9AlGUMNqd/j4VZtQzA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.798.0.tgz", + "integrity": "sha512-Si4W7kFflNXC48lr05n2Fc5nrD6whbfgR7c5/7hYSXP52DOqy2kMle+bZx5EkmQ/e/5nAPW0DS4ABeLprVSghw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -263,17 +263,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.796.0.tgz", - "integrity": "sha512-tH8Sp7lCxISVoLnkyv4AouuXs2CDlMhTuesWa0lq2NX1f+DXsMwSBtN37ttZdpFMw3F8mWdsJt27X9h2Oq868A==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.798.0.tgz", + "integrity": "sha512-hITxDE4pVkeJqz0LXjQRDgR+noxJ5oOxG38fgmQXjPXsdwVKnNIiMJ5S2WFMVSszU7ebGSyHdPHENQKu6TReVA==", "dependencies": { "@aws-sdk/types": "3.775.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", @@ -305,11 +305,11 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.796.0.tgz", - "integrity": "sha512-kQzGKm4IOYYO6vUrai2JocNwhJm4Aml2BsAV+tBhFhhkutE7khf9PUucoVjB78b0J48nF+kdSacqzY+gB81/Uw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.798.0.tgz", + "integrity": "sha512-EsfzTEeoaHY1E+g3S6AmC3bF6euZN5SrLcLh5Oxhx5q2qjWUsKEK0fwek+jlt2GH7zB3F9IArV4z+8CsDQdKYw==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -320,17 +320,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.796.0.tgz", - "integrity": "sha512-wWOT6VAHIKOuHdKFGm1iyKvx7f6+Kc/YTzFWJPuT+l+CPlXR6ylP1UMIDsHHLKpMzsrh3CH77QDsjkhQrnKkfg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.798.0.tgz", + "integrity": "sha512-bw5TmcJqpBVQlXzkL63545iHQ9mxwQeXTS/rgUQ5rmNNS3yiGDekVZOLXo/Gs4wmt2/59UN/sWIRFxvxDpMQEg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" @@ -340,17 +340,17 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.796.0.tgz", - "integrity": "sha512-qGWBDn9aO8avFfYU7daps7Sy6OglF1x0q0w48slt0KMXbHd2/LvKVIiYwyofYCXed0yzcEOF2IYm9FjXdcn+ug==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.798.0.tgz", + "integrity": "sha512-zqWwKhhdf5CVRL6+4vNNTZVHWH9OiiwUWA3ka44jJaAMBRbbryjRedzwkWbgDaL1EbfTbcBZTYzE7N/vK7UUVA==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -363,16 +363,16 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.796.0.tgz", - "integrity": "sha512-WeNK7OWPrsOvhO3DAgpUO0FtmVghMaZ/IpPJHJ4Y0nBIsWOBXLrbZ2Y1mdT8N2bGGUaM91tJaV8Yf8COc3gvmA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.798.0.tgz", + "integrity": "sha512-Mrhl4wS4lMpuw2NCga5/rtQehNfyRs8NUHfvrLK5bZvJbjanrh8QtdRVhrAjw71OwFh3GK49QMByGkUssALJ+g==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-ini": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-ini": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -385,11 +385,11 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.796.0.tgz", - "integrity": "sha512-r4e8/4AdKn/qQbRVocW7oXkpoiuXdTv0qty8AASNLnbQnT1vjD1bvmP6kp4fbHPWgwY8I9h0Dqjp49uy9Bqyuw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.798.0.tgz", + "integrity": "sha512-BbRq8bhCHC94OTRIg5edgGTaWUzBH0h/IZJZ0vERle8A9nfl+5jUplvC8cvh3/8cNgHIRXj5HzlDjeSVe9dySg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -401,13 +401,13 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.796.0.tgz", - "integrity": "sha512-RUYsQ1t6UdzkpZ7pocUt1l/9l9GCYCaopIhv0DU6CipA8rkWtoweKsLHKdv+8wE4p6gqDfDIHGam1ivswiCIzg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.798.0.tgz", + "integrity": "sha512-MLpQRb7xkqI9w0slEA76QiHGzM0PDMcpVcQG0wFHrpLKkQYjYlD9H3VfxdYGUh+FPOaR1fFpRZb18Gz9MR/2eQ==", "dependencies": { - "@aws-sdk/client-sso": "3.796.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/token-providers": "3.796.0", + "@aws-sdk/client-sso": "3.798.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/token-providers": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -419,12 +419,12 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.796.0.tgz", - "integrity": "sha512-dpmFJT4IyjT09vruvMu/rWQQjVreqdxAe8pLPpGhoeKyA1O6+PS73b+VNXKvD31rQT8e4g6dVpA6KMxNW63aag==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.798.0.tgz", + "integrity": "sha512-OWBDy/ZiC0pxLzp1Nhah5jxDZ/onLTjouIVGPyc9E8/KzUJxqQbR6fk43VqhpYdVp/S7yDDbaOpO072RRZJQrw==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -476,14 +476,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.796.0.tgz", - "integrity": "sha512-IeNg+3jNWT37J45opi5Jx89hGF0lOnZjiNwlMp3rKq7PlOqy8kWq5J1Gxk0W3tIkPpuf68CtBs/QFrRXWOjsZw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.798.0.tgz", + "integrity": "sha512-nb3YvLokpu/2meKVH5hGVLNg+hz3IyFCESEJW+SpK7bW/SfaKpukGY1lqwqbf+edl+s20MRXeK/by1rvBChixQ==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" @@ -493,43 +493,43 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.796.0.tgz", - "integrity": "sha512-jJ8a0ldWtXh/ice7nldUjTqja7KYlSYk1pwfIIvJLIqEn2SvQHK/pyCINTmmOmFAWXMKBQBeWUMxo1pPYNytzQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.798.0.tgz", + "integrity": "sha512-14iBJgg2Qqf74IeUY+z1nP5GIJIBZj8lv9mdpXrHlK8k+FcMXjpHg/B+JguSMhb2sbLeb5N0H8HLJGIRNALVWw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -557,11 +557,11 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.796.0.tgz", - "integrity": "sha512-Sxr/EqJBxOwLsXHv8C91N/Aao8Rgjn5bcpzplrTZ7wrfDrzqQfSCvjh7apCxdLYMKPBV+an75blCAd7JD4/bAg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.798.0.tgz", + "integrity": "sha512-iYhNmHXfWLUwcMP9ldb/H+RMRLHZbBUWBgsoQqfb7sl6z24nH0qBJyL+oXHTCVBUYLP20CvUrVkcwlejDzyoRw==", "dependencies": { - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -621,11 +621,11 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.796.0.tgz", - "integrity": "sha512-9fQpNcHgVFitf1tbTT8V1xGRoRHSmOAWjrhevo6Tc0WoINMAKz+4JNqfVGWRE5Tmtpq0oHKo1RmvxXQQtJYciA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.798.0.tgz", + "integrity": "sha512-yncgNd2inI+y5kdfn2i0oBwgCxwdtcVShNNVQ+5b/nuC1Lgjgcb+hmHAeTFMge7vhDP2Md8I+ih6bPMpK79lQQ==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -2062,9 +2062,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.2.0.tgz", - "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.0.tgz", + "integrity": "sha512-r6gvs5OfRq/w+9unPm7B3po4rmWaGh0CIL/OwHntGGux7+RhOOZLGuurbeMgWV6W55ZuyMTypJLeH0vn/ZRaWQ==", "dependencies": { "@smithy/middleware-serde": "^4.0.3", "@smithy/protocol-http": "^5.1.0", @@ -2160,11 +2160,11 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz", - "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.1.tgz", + "integrity": "sha512-z5RmcHxjvScL+LwEDU2mTNCOhgUs4lu5PGdF1K36IPRmUHhNFxNxgenSB7smyDiYD4vdKQ7CAZtG5cUErqib9w==", "dependencies": { - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/node-config-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -2178,14 +2178,14 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz", - "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.1.tgz", + "integrity": "sha512-mBJOxn9aUYwcBUPQpKv9ifzrCn4EbhPUFguEZv3jB57YOMh0caS4P8HoLvUeNUI1nx4bIVH2SIbogbDfFI9DUA==", "dependencies": { "@smithy/node-config-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/service-error-classification": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -2352,12 +2352,12 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.0.tgz", - "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.1.tgz", + "integrity": "sha512-fbniZef60QdsBc4ZY0iyI8xbFHIiC/QRtPi66iE4ufjiE/aaz7AfUXzcWMkpO8r+QhLeNRIfmPchIG+3/QDZ6g==", "dependencies": { - "@smithy/core": "^3.2.0", - "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/core": "^3.3.0", + "@smithy/middleware-endpoint": "^4.1.1", "@smithy/middleware-stack": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", @@ -2451,12 +2451,12 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz", - "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.9.tgz", + "integrity": "sha512-B8j0XsElvyhv6+5hlFf6vFV/uCSyLKcInpeXOGnOImX2mGXshE01RvPoGipTlRpIk53e6UfYj7WdDdgbVfXDZw==", "dependencies": { "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -2466,15 +2466,15 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz", - "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.9.tgz", + "integrity": "sha512-wTDU8P/zdIf9DOpV5qm64HVgGRXvqjqB/fJZTEQbrz3s79JHM/E7XkMm/876Oq+ZLHJQgnXM9QHDo29dlM62eA==", "dependencies": { "@smithy/config-resolver": "^4.1.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, diff --git a/functions/package.json b/functions/package.json index 4d2e4a9..59f0fdf 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,7 +15,7 @@ }, "main": "lib/index.js", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", diff --git a/functions/src/index.ts b/functions/src/index.ts index a0fb365..1c5b36c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; -export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; +export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; diff --git a/functions/src/payments/index.ts b/functions/src/payments/index.ts index 8d52c66..beda940 100644 --- a/functions/src/payments/index.ts +++ b/functions/src/payments/index.ts @@ -1 +1,2 @@ export * from './cashfree'; +export * from './phonepe'; diff --git a/functions/src/payments/phonepe/checkStatus.ts b/functions/src/payments/phonepe/checkStatus.ts new file mode 100644 index 0000000..df2fa36 --- /dev/null +++ b/functions/src/payments/phonepe/checkStatus.ts @@ -0,0 +1,139 @@ +import axios from "axios"; +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const checkPhonePePaymentStatus = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + await admin.auth().verifyIdToken(idToken); + + const merchantOrderId = request.query.merchantOrderId as string; + if (!merchantOrderId) { + response.status(400).json({ error: 'Missing merchant order ID' }); + return; + } + + const details = request.query.details === 'true'; + const errorContext = request.query.errorContext === 'true'; + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const queryParams = new URLSearchParams(); + if (details) queryParams.append('details', 'true'); + if (errorContext) queryParams.append('errorContext', 'true'); + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; + + const statusResponse = await axios.get( + `${apiUrl}/checkout/v2/order/${merchantOrderId}/status${queryString}`, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', merchantOrderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found', + message: `No record found for PhonePe order ID: ${merchantOrderId}` + }); + return; + } + + const orderDoc = orderQuery.docs[0]; + + await orderDoc.ref.update({ + orderStatus: statusResponse.data.state || 'UNKNOWN', + lastChecked: new Date(), + statusResponse: statusResponse.data + }); + logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + + response.json({ + success: true, + state: statusResponse.data.state, + data: statusResponse.data + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + + if (authError.response) { + logger.error('API error details:', { + status: authError.response.status, + data: authError.response.data + }); + + response.status(authError.response.status).json({ + success: false, + error: 'API error', + details: authError.response.data + }); + } else { + response.status(401).json({ + success: false, + error: 'Invalid authentication token or API error', + message: authError.message + }); + } + } + } catch (error: any) { + logger.error('PhonePe payment status check error:', error); + response.status(500).json({ + success: false, + error: 'Failed to check payment status', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/createPhonepeOrder.ts b/functions/src/payments/phonepe/createPhonepeOrder.ts new file mode 100644 index 0000000..fcad1c2 --- /dev/null +++ b/functions/src/payments/phonepe/createPhonepeOrder.ts @@ -0,0 +1,133 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const createPhonePeOrder = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + + const { + merchantOrderId, + amount, + expireAfter, + metaInfo, + paymentFlow + } = request.body; + + if (!merchantOrderId || !amount || !paymentFlow || !expireAfter) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + if (!paymentFlow.type || !paymentFlow.merchantUrls || !paymentFlow.merchantUrls.redirectUrl) { + response.status(400).json({ error: 'Invalid payment flow configuration' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + try { + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const paymentResponse = await axios.post( + `${apiUrl}/checkout/v2/pay`, + request.body, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + try { + await admin.firestore().collection('payment_orders').doc(merchantOrderId).set({ + userId: uid, + amount: amount / 100, + orderStatus: paymentResponse.data.state || 'PENDING', + paymentGateway: 'PhonePe', + createdAt: new Date(), + merchantOrderId: merchantOrderId, + paymentUrl: paymentResponse.data.redirectUrl, + orderId: paymentResponse.data.orderId, + expireAt: new Date(paymentResponse.data.expireAt), + // rawResponse: paymentResponse.data, + metaInfo: metaInfo || {} + }); + } catch (firestoreError) { + logger.error('Error storing order in Firestore:', firestoreError); + } + + response.json({ + ...paymentResponse.data, + merchantOrderId: merchantOrderId + }); + + logger.info(`PhonePe order created: ${merchantOrderId}`); + } catch (apiError: any) { + logger.error('PhonePe API error:', apiError); + response.status(apiError.response?.status || 500).json({ + success: false, + error: 'Payment gateway error', + details: apiError.response?.data || apiError.message, + code: apiError.code + }); + } + } catch (authError) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token' + }); + } + } catch (error: any) { + logger.error('PhonePe order creation error:', error); + response.status(500).json({ + success: false, + error: 'Failed to create payment order', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts new file mode 100644 index 0000000..2fe0eda --- /dev/null +++ b/functions/src/payments/phonepe/index.ts @@ -0,0 +1,5 @@ +export { createPhonePeOrder } from './createPhonepeOrder'; +export { checkPhonePePaymentStatus } from './checkStatus'; +// export { createPhonePePaymentLink } from './paymentLink'; +export { phonePeWebhook } from './webhook'; +// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus'; diff --git a/functions/src/payments/phonepe/paymentLink.ts b/functions/src/payments/phonepe/paymentLink.ts new file mode 100644 index 0000000..a676868 --- /dev/null +++ b/functions/src/payments/phonepe/paymentLink.ts @@ -0,0 +1,183 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +interface PaymentLinkPayload { + merchantId: string; + merchantOrderId: string; + merchantUserId: string; + amount: number; + mobileNumber?: string; + email?: string; + shortName: string; + expiryDate: number; + redirectUrl: string; + redirectMode: string; + paymentInstrument: { + type: string; + }; + notifyCustomer?: boolean; +} + +export const createPhonePePaymentLink = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + + const { + amount, + orderId, + customerName, + customerEmail, + customerPhone, + productInfo, + expiryDays = 7, + callbackUrl, + notifyCustomer = false + } = request.body; + + if (!amount || !orderId) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const merchantId = process.env.PHONEPE_MERCHANT_ID; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl || !merchantId) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + try { + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const expiryInSeconds = Math.floor(Date.now() / 1000) + (expiryDays * 24 * 60 * 60); + + const paymentLinkPayload: PaymentLinkPayload = { + merchantId: merchantId, + merchantOrderId: orderId, + merchantUserId: uid, + amount: parseInt(amount) * 100, + shortName: productInfo || "Payment", + expiryDate: expiryInSeconds, + redirectUrl: callbackUrl, + redirectMode: "REDIRECT", + paymentInstrument: { + type: "PAY_PAGE" + } + }; + + if (customerPhone) { + paymentLinkPayload.mobileNumber = customerPhone; + } + + if (customerEmail) { + paymentLinkPayload.email = customerEmail; + } + + if (notifyCustomer && (customerEmail || customerPhone)) { + paymentLinkPayload.notifyCustomer = true; + } + + const paymentLinkResponse = await axios.post( + `${apiUrl}/v3/payment-links/create`, + paymentLinkPayload, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + } + ); + + try { + await admin.firestore().collection('payment_links').doc(orderId).set({ + userId: uid, + amount: amount, + customerName: customerName, + customerEmail: customerEmail, + customerPhone: customerPhone, + orderStatus: 'CREATED', + paymentGateway: 'PhonePe', + createdAt: new Date(), + expiryDate: new Date(expiryInSeconds * 1000), + orderId: orderId, + paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, + paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, + rawResponse: paymentLinkResponse.data + }); + } catch (firestoreError) { + logger.error('Error storing payment link in Firestore:', firestoreError); + } + + response.json({ + success: true, + orderId: orderId, + paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, + paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, + response: paymentLinkResponse.data + }); + + logger.info(`PhonePe payment link created: ${orderId}`); + } catch (apiError: any) { + logger.error('PhonePe API error:', apiError.response?.data || apiError); + response.status(apiError.response?.status || 500).json({ + success: false, + error: 'Payment gateway error', + details: apiError.response?.data || apiError.message, + code: apiError.code + }); + } + } catch (authError) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token' + }); + } + } catch (error: any) { + logger.error('PhonePe payment link creation error:', error); + response.status(500).json({ + success: false, + error: 'Failed to create payment link', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/paymentLinkStatus.ts b/functions/src/payments/phonepe/paymentLinkStatus.ts new file mode 100644 index 0000000..1364fe4 --- /dev/null +++ b/functions/src/payments/phonepe/paymentLinkStatus.ts @@ -0,0 +1,122 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const checkPhonePePaymentLinkStatus = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + await admin.auth().verifyIdToken(idToken); + + const linkId = request.query.linkId as string; + if (!linkId) { + response.status(400).json({ error: 'Missing payment link ID' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const statusResponse = await axios.get( + `${apiUrl}/v3/payment-links/${linkId}/status`, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + } + ); + + const linkQuery = await admin.firestore() + .collection('payment_links') + .where('paymentLinkId', '==', linkId) + .limit(1) + .get(); + + if (!linkQuery.empty) { + const linkDoc = linkQuery.docs[0]; + + await linkDoc.ref.update({ + orderStatus: statusResponse.data.data?.state || statusResponse.data.state || 'UNKNOWN', + lastChecked: new Date(), + statusResponse: statusResponse.data + }); + } + + response.json({ + success: true, + state: statusResponse.data.data?.state || statusResponse.data.state, + data: statusResponse.data + }); + + } catch (authError: any) { + logger.error('Authentication or API error:', authError); + + if (authError.response) { + logger.error('API error details:', { + status: authError.response.status, + data: authError.response.data + }); + + response.status(authError.response.status).json({ + success: false, + error: 'API error', + details: authError.response.data + }); + } else { + response.status(401).json({ + success: false, + error: 'Invalid authentication token or API error', + message: authError.message + }); + } + } + } catch (error: any) { + logger.error('PhonePe payment link status check error:', error); + response.status(500).json({ + success: false, + error: 'Failed to check payment link status', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts new file mode 100644 index 0000000..5f52274 --- /dev/null +++ b/functions/src/payments/phonepe/webhook.ts @@ -0,0 +1,102 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getAdmin, getLogger } from "../../shared/config"; +import crypto from "crypto"; + +const admin = getAdmin(); +const logger = getLogger(); + +export const phonePeWebhook = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + try { + const signature = request.headers['x-verify'] as string; + const webhookSecret = process.env.PHONEPE_WEBHOOK_SECRET; + + if (!signature || !webhookSecret) { + logger.error('Missing signature or webhook secret'); + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const rawBody = JSON.stringify(request.body); + + const expectedSignature = crypto + .createHmac('sha256', webhookSecret) + .update(rawBody) + .digest('hex'); + + if (signature !== expectedSignature) { + logger.error('Invalid webhook signature'); + response.status(401).json({ error: 'Invalid signature' }); + return; + } + + const { event, data } = request.body; + + if (!event || !data || !data.merchantOrderId || !data.orderId) { + logger.error('Invalid webhook payload', request.body); + response.status(400).json({ error: 'Invalid payload' }); + return; + } + + logger.info(`Received PhonePe webhook: ${event}`, { + merchantOrderId: data.merchantOrderId, + orderId: data.orderId, + state: data.state + }); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('orderId', '==', data.orderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + const merchantOrderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', data.merchantOrderId) + .limit(1) + .get(); + + if (merchantOrderQuery.empty) { + logger.error(`No payment order found for PhonePe orderId: ${data.orderId} or merchantOrderId: ${data.merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found' + }); + return; + } + + const orderDoc = merchantOrderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: data.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: data + }); + + logger.info(`Updated order status via webhook for merchantOrderId: ${data.merchantOrderId} to ${data.state}`); + } else { + const orderDoc = orderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: data.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: data + }); + + logger.info(`Updated order status via webhook for orderId: ${data.orderId} to ${data.state}`); + } + + response.status(200).json({ success: true }); + + } catch (error: any) { + logger.error('PhonePe webhook processing error:', error); + response.status(500).json({ + success: false, + error: 'Failed to process webhook', + details: error.message + }); + } +}); diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 752b2ab..ff92031 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,7 +9,9 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "target": "es2017" + "target": "es2017", + "types": [], + "skipLibCheck": true }, "compileOnSave": true, "include": [ diff --git a/package.json b/package.json index 5f79027..474c153 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,5 @@ "dependencies": { "@types/busboy": "^1.5.4", "busboy": "^1.6.0" - }, - "devDependencies": { - "@types/long": "^5.0.0" } } From c28597f3ee49f351b2f8a4c76b9bd712665ca57a Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 5 May 2025 17:59:23 +0530 Subject: [PATCH 28/81] phonepe function completed --- firebase.json | 2 +- functions/src/payments/phonepe/index.ts | 2 - functions/src/payments/phonepe/paymentLink.ts | 183 ------------------ .../src/payments/phonepe/paymentLinkStatus.ts | 122 ------------ functions/src/payments/phonepe/webhook.ts | 56 +++--- 5 files changed, 31 insertions(+), 334 deletions(-) delete mode 100644 functions/src/payments/phonepe/paymentLink.ts delete mode 100644 functions/src/payments/phonepe/paymentLinkStatus.ts diff --git a/firebase.json b/firebase.json index 0eb205c..d8682a0 100644 --- a/firebase.json +++ b/firebase.json @@ -25,7 +25,7 @@ "port": 5005 }, "firestore": { - "port": 8081 + "port": 8085 }, "storage": { "port": 9199 diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts index 2fe0eda..258f121 100644 --- a/functions/src/payments/phonepe/index.ts +++ b/functions/src/payments/phonepe/index.ts @@ -1,5 +1,3 @@ export { createPhonePeOrder } from './createPhonepeOrder'; export { checkPhonePePaymentStatus } from './checkStatus'; -// export { createPhonePePaymentLink } from './paymentLink'; export { phonePeWebhook } from './webhook'; -// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus'; diff --git a/functions/src/payments/phonepe/paymentLink.ts b/functions/src/payments/phonepe/paymentLink.ts deleted file mode 100644 index a676868..0000000 --- a/functions/src/payments/phonepe/paymentLink.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import { getCorsHandler } from "../../shared/middleware"; -import { getAdmin, getLogger } from "../../shared/config"; -import axios from "axios"; -const admin = getAdmin(); -const logger = getLogger(); -const corsHandler = getCorsHandler(); - -interface PaymentLinkPayload { - merchantId: string; - merchantOrderId: string; - merchantUserId: string; - amount: number; - mobileNumber?: string; - email?: string; - shortName: string; - expiryDate: number; - redirectUrl: string; - redirectMode: string; - paymentInstrument: { - type: string; - }; - notifyCustomer?: boolean; -} - -export const createPhonePePaymentLink = onRequest({ - region: '#{SERVICES_RGN}#' - }, async (request: Request, response) => { - return corsHandler(request, response, async () => { - try { - const authHeader = request.headers.authorization || ''; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - response.status(401).json({ error: 'Unauthorized' }); - return; - } - - const idToken = authHeader.split('Bearer ')[1]; - try { - const decodedToken = await admin.auth().verifyIdToken(idToken); - const uid = decodedToken.uid; - - const { - amount, - orderId, - customerName, - customerEmail, - customerPhone, - productInfo, - expiryDays = 7, - callbackUrl, - notifyCustomer = false - } = request.body; - - if (!amount || !orderId) { - response.status(400).json({ error: 'Missing required fields' }); - return; - } - - const clientId = process.env.PHONEPE_CLIENT_ID; - const clientSecret = process.env.PHONEPE_CLIENT_SECRET; - const apiUrl = process.env.PHONEPE_API_URL; - const merchantId = process.env.PHONEPE_MERCHANT_ID; - const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; - - if (!clientId || !clientSecret || !apiUrl || !merchantId) { - logger.error('PhonePe credentials not configured'); - response.status(500).json({ error: 'Payment gateway configuration error' }); - return; - } - - try { - const tokenResponse = await axios.post( - `${apiUrl}/v1/oauth/token`, - { - client_id: clientId, - client_version: clientVersion, - client_secret: clientSecret, - grant_type: 'client_credentials', - }, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - } - ); - - const accessToken = tokenResponse.data.access_token; - - const expiryInSeconds = Math.floor(Date.now() / 1000) + (expiryDays * 24 * 60 * 60); - - const paymentLinkPayload: PaymentLinkPayload = { - merchantId: merchantId, - merchantOrderId: orderId, - merchantUserId: uid, - amount: parseInt(amount) * 100, - shortName: productInfo || "Payment", - expiryDate: expiryInSeconds, - redirectUrl: callbackUrl, - redirectMode: "REDIRECT", - paymentInstrument: { - type: "PAY_PAGE" - } - }; - - if (customerPhone) { - paymentLinkPayload.mobileNumber = customerPhone; - } - - if (customerEmail) { - paymentLinkPayload.email = customerEmail; - } - - if (notifyCustomer && (customerEmail || customerPhone)) { - paymentLinkPayload.notifyCustomer = true; - } - - const paymentLinkResponse = await axios.post( - `${apiUrl}/v3/payment-links/create`, - paymentLinkPayload, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - } - ); - - try { - await admin.firestore().collection('payment_links').doc(orderId).set({ - userId: uid, - amount: amount, - customerName: customerName, - customerEmail: customerEmail, - customerPhone: customerPhone, - orderStatus: 'CREATED', - paymentGateway: 'PhonePe', - createdAt: new Date(), - expiryDate: new Date(expiryInSeconds * 1000), - orderId: orderId, - paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, - paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, - rawResponse: paymentLinkResponse.data - }); - } catch (firestoreError) { - logger.error('Error storing payment link in Firestore:', firestoreError); - } - - response.json({ - success: true, - orderId: orderId, - paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId, - paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl, - response: paymentLinkResponse.data - }); - - logger.info(`PhonePe payment link created: ${orderId}`); - } catch (apiError: any) { - logger.error('PhonePe API error:', apiError.response?.data || apiError); - response.status(apiError.response?.status || 500).json({ - success: false, - error: 'Payment gateway error', - details: apiError.response?.data || apiError.message, - code: apiError.code - }); - } - } catch (authError) { - logger.error('Authentication error:', authError); - response.status(401).json({ - success: false, - error: 'Invalid authentication token' - }); - } - } catch (error: any) { - logger.error('PhonePe payment link creation error:', error); - response.status(500).json({ - success: false, - error: 'Failed to create payment link', - details: error.message - }); - } - }); - }); diff --git a/functions/src/payments/phonepe/paymentLinkStatus.ts b/functions/src/payments/phonepe/paymentLinkStatus.ts deleted file mode 100644 index 1364fe4..0000000 --- a/functions/src/payments/phonepe/paymentLinkStatus.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import { getCorsHandler } from "../../shared/middleware"; -import { getAdmin, getLogger } from "../../shared/config"; -import axios from "axios"; -const admin = getAdmin(); -const logger = getLogger(); -const corsHandler = getCorsHandler(); - -export const checkPhonePePaymentLinkStatus = onRequest({ - region: '#{SERVICES_RGN}#' -}, async (request: Request, response) => { - return corsHandler(request, response, async () => { - try { - const authHeader = request.headers.authorization || ''; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - response.status(401).json({ error: 'Unauthorized' }); - return; - } - - const idToken = authHeader.split('Bearer ')[1]; - try { - await admin.auth().verifyIdToken(idToken); - - const linkId = request.query.linkId as string; - if (!linkId) { - response.status(400).json({ error: 'Missing payment link ID' }); - return; - } - - const clientId = process.env.PHONEPE_CLIENT_ID; - const clientSecret = process.env.PHONEPE_CLIENT_SECRET; - const apiUrl = process.env.PHONEPE_API_URL; - const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; - - if (!clientId || !clientSecret || !apiUrl) { - logger.error('PhonePe credentials not configured'); - response.status(500).json({ error: 'Payment gateway configuration error' }); - return; - } - - const tokenResponse = await axios.post( - `${apiUrl}/v1/oauth/token`, - { - client_id: clientId, - client_version: clientVersion, - client_secret: clientSecret, - grant_type: 'client_credentials', - }, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - } - ); - - const accessToken = tokenResponse.data.access_token; - - const statusResponse = await axios.get( - `${apiUrl}/v3/payment-links/${linkId}/status`, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - } - ); - - const linkQuery = await admin.firestore() - .collection('payment_links') - .where('paymentLinkId', '==', linkId) - .limit(1) - .get(); - - if (!linkQuery.empty) { - const linkDoc = linkQuery.docs[0]; - - await linkDoc.ref.update({ - orderStatus: statusResponse.data.data?.state || statusResponse.data.state || 'UNKNOWN', - lastChecked: new Date(), - statusResponse: statusResponse.data - }); - } - - response.json({ - success: true, - state: statusResponse.data.data?.state || statusResponse.data.state, - data: statusResponse.data - }); - - } catch (authError: any) { - logger.error('Authentication or API error:', authError); - - if (authError.response) { - logger.error('API error details:', { - status: authError.response.status, - data: authError.response.data - }); - - response.status(authError.response.status).json({ - success: false, - error: 'API error', - details: authError.response.data - }); - } else { - response.status(401).json({ - success: false, - error: 'Invalid authentication token or API error', - message: authError.message - }); - } - } - } catch (error: any) { - logger.error('PhonePe payment link status check error:', error); - response.status(500).json({ - success: false, - error: 'Failed to check payment link status', - details: error.message - }); - } - }); -}); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 5f52274..d8696dc 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -10,57 +10,61 @@ export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { - const signature = request.headers['x-verify'] as string; - const webhookSecret = process.env.PHONEPE_WEBHOOK_SECRET; + const authHeader = request.headers['authorization'] as string; + const username = process.env.PHONEPE_WEBHOOK_USERNAME; + const password = process.env.PHONEPE_WEBHOOK_PASSWORD; - if (!signature || !webhookSecret) { - logger.error('Missing signature or webhook secret'); + if (!authHeader || !username || !password) { + logger.error('Missing authorization header or webhook credentials'); response.status(401).json({ error: 'Unauthorized' }); return; } - const rawBody = JSON.stringify(request.body); - - const expectedSignature = crypto - .createHmac('sha256', webhookSecret) - .update(rawBody) + // Calculate expected authorization value + const credentialString = `${username}:${password}`; + const expectedAuth = crypto + .createHash('sha256') + .update(credentialString) .digest('hex'); - if (signature !== expectedSignature) { - logger.error('Invalid webhook signature'); - response.status(401).json({ error: 'Invalid signature' }); + // PhonePe may send the header with a prefix like "SHA256 " or just the hash + const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); + + if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { + logger.error('Invalid webhook authorization'); + response.status(401).json({ error: 'Invalid authorization' }); return; } - const { event, data } = request.body; + const { event, payload } = request.body; - if (!event || !data || !data.merchantOrderId || !data.orderId) { + if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { logger.error('Invalid webhook payload', request.body); response.status(400).json({ error: 'Invalid payload' }); return; } logger.info(`Received PhonePe webhook: ${event}`, { - merchantOrderId: data.merchantOrderId, - orderId: data.orderId, - state: data.state + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + state: payload.state }); const orderQuery = await admin.firestore() .collection('payment_orders') - .where('orderId', '==', data.orderId) + .where('orderId', '==', payload.orderId) .limit(1) .get(); if (orderQuery.empty) { const merchantOrderQuery = await admin.firestore() .collection('payment_orders') - .where('merchantOrderId', '==', data.merchantOrderId) + .where('merchantOrderId', '==', payload.merchantOrderId) .limit(1) .get(); if (merchantOrderQuery.empty) { - logger.error(`No payment order found for PhonePe orderId: ${data.orderId} or merchantOrderId: ${data.merchantOrderId}`); + logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); response.status(404).json({ success: false, error: 'Payment order not found' @@ -70,23 +74,23 @@ export const phonePeWebhook = onRequest({ const orderDoc = merchantOrderQuery.docs[0]; await orderDoc.ref.update({ - orderStatus: data.state || 'UNKNOWN', + orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, - webhookData: data + webhookData: payload }); - logger.info(`Updated order status via webhook for merchantOrderId: ${data.merchantOrderId} to ${data.state}`); + logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); } else { const orderDoc = orderQuery.docs[0]; await orderDoc.ref.update({ - orderStatus: data.state || 'UNKNOWN', + orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, - webhookData: data + webhookData: payload }); - logger.info(`Updated order status via webhook for orderId: ${data.orderId} to ${data.state}`); + logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } response.status(200).json({ success: true }); From 54006c44cfe00bf9af5db54c4a51b06b26e07b54 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 5 May 2025 18:12:30 +0530 Subject: [PATCH 29/81] Update .env.example --- functions/.env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/functions/.env.example b/functions/.env.example index 397672a..e62bb23 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -11,6 +11,11 @@ GOOGLE_MAPS_API_KEY=#{GOOGLE_MAPS_API_KEY}# CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# +PHONEPE_CLIENT_ID=#{PHONEPE_CLIENT_ID}# +PHONEPE_CLIENT_SECRET=#{PHONEPE_CLIENT_SECRET}# +PHONEPE_API_URL=#{PHONEPE_API_URL}# +PHONEPE_WEBHOOK_USERNAME=#{PHONEPE_WEBHOOK_USERNAME}# +PHONEPE_WEBHOOK_PASSWORD=#{PHONEPE_WEBHOOK_PASSWORD}# SES_FROM_EMAIL=#{SES_FROM_EMAIL}# SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL}# From 9efa31b6cce3966b4118fda0cb0e5f88cda4163e Mon Sep 17 00:00:00 2001 From: Allen T J Date: Mon, 5 May 2025 14:09:08 +0000 Subject: [PATCH 30/81] phonepe (#23) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/23 Co-authored-by: Allen T J Co-committed-by: Allen T J --- firebase.json | 6 +- functions/.env.example | 5 + functions/package-lock.json | 246 +++++++++--------- functions/package.json | 2 +- functions/src/index.ts | 2 +- functions/src/payments/index.ts | 1 + functions/src/payments/phonepe/checkStatus.ts | 139 ++++++++++ .../payments/phonepe/createPhonepeOrder.ts | 133 ++++++++++ functions/src/payments/phonepe/index.ts | 3 + functions/src/payments/phonepe/webhook.ts | 106 ++++++++ functions/tsconfig.json | 4 +- package.json | 3 - 12 files changed, 518 insertions(+), 132 deletions(-) create mode 100644 functions/src/payments/phonepe/checkStatus.ts create mode 100644 functions/src/payments/phonepe/createPhonepeOrder.ts create mode 100644 functions/src/payments/phonepe/index.ts create mode 100644 functions/src/payments/phonepe/webhook.ts diff --git a/firebase.json b/firebase.json index dd93bf2..d8682a0 100644 --- a/firebase.json +++ b/firebase.json @@ -22,17 +22,17 @@ }, "emulators": { "functions": { - "port": 5001 + "port": 5005 }, "firestore": { - "port": 8079 + "port": 8085 }, "storage": { "port": 9199 }, "ui": { "enabled": true, - "port": 4000 + "port": 4008 }, "auth": { "port": 9099 diff --git a/functions/.env.example b/functions/.env.example index 397672a..e62bb23 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -11,6 +11,11 @@ GOOGLE_MAPS_API_KEY=#{GOOGLE_MAPS_API_KEY}# CASHFREE_URL=#{CASHFREE_URL}# CASHFREE_LINK_URL=#{CASHFREE_LINK_URL}# CASHFREE_LINK_NOTIFY_URL=#{CASHFREE_LINK_NOTIFY_URL}# +PHONEPE_CLIENT_ID=#{PHONEPE_CLIENT_ID}# +PHONEPE_CLIENT_SECRET=#{PHONEPE_CLIENT_SECRET}# +PHONEPE_API_URL=#{PHONEPE_API_URL}# +PHONEPE_WEBHOOK_USERNAME=#{PHONEPE_WEBHOOK_USERNAME}# +PHONEPE_WEBHOOK_PASSWORD=#{PHONEPE_WEBHOOK_PASSWORD}# SES_FROM_EMAIL=#{SES_FROM_EMAIL}# SES_REPLY_TO_EMAIL=#{SES_REPLY_TO_EMAIL}# diff --git a/functions/package-lock.json b/functions/package-lock.json index d37a055..28053b5 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,7 +8,7 @@ "name": "functions", "version": "0.0.0", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -165,44 +165,44 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.796.0.tgz", - "integrity": "sha512-gg1z0uXtm6v3wCMrGM5eCh69aWT+VOzYvNdbSMA5EPLfW9qsOCH1Zz/q9XzUZjWN25OnGvfm1LzypzQgRypSeQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.798.0.tgz", + "integrity": "sha512-s12ntssTgau7ySHoOXVAliVM1a1JyjsFYwauKh/7gCriJ+lIBlo25BFgCjZAHxxBn2FfCete6BVQFEbN/Ishkg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-node": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-node": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -215,43 +215,43 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.796.0.tgz", - "integrity": "sha512-EJExg8mbwqP0VG+RNFV4ZPuUo7QsDsUfTnuFQY51V8iXrbOdV+PDLRr4psXj2fxvrLxc9AlGUMNqd/j4VZtQzA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.798.0.tgz", + "integrity": "sha512-Si4W7kFflNXC48lr05n2Fc5nrD6whbfgR7c5/7hYSXP52DOqy2kMle+bZx5EkmQ/e/5nAPW0DS4ABeLprVSghw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -263,17 +263,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.796.0.tgz", - "integrity": "sha512-tH8Sp7lCxISVoLnkyv4AouuXs2CDlMhTuesWa0lq2NX1f+DXsMwSBtN37ttZdpFMw3F8mWdsJt27X9h2Oq868A==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.798.0.tgz", + "integrity": "sha512-hITxDE4pVkeJqz0LXjQRDgR+noxJ5oOxG38fgmQXjPXsdwVKnNIiMJ5S2WFMVSszU7ebGSyHdPHENQKu6TReVA==", "dependencies": { "@aws-sdk/types": "3.775.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", @@ -305,11 +305,11 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.796.0.tgz", - "integrity": "sha512-kQzGKm4IOYYO6vUrai2JocNwhJm4Aml2BsAV+tBhFhhkutE7khf9PUucoVjB78b0J48nF+kdSacqzY+gB81/Uw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.798.0.tgz", + "integrity": "sha512-EsfzTEeoaHY1E+g3S6AmC3bF6euZN5SrLcLh5Oxhx5q2qjWUsKEK0fwek+jlt2GH7zB3F9IArV4z+8CsDQdKYw==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -320,17 +320,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.796.0.tgz", - "integrity": "sha512-wWOT6VAHIKOuHdKFGm1iyKvx7f6+Kc/YTzFWJPuT+l+CPlXR6ylP1UMIDsHHLKpMzsrh3CH77QDsjkhQrnKkfg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.798.0.tgz", + "integrity": "sha512-bw5TmcJqpBVQlXzkL63545iHQ9mxwQeXTS/rgUQ5rmNNS3yiGDekVZOLXo/Gs4wmt2/59UN/sWIRFxvxDpMQEg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" @@ -340,17 +340,17 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.796.0.tgz", - "integrity": "sha512-qGWBDn9aO8avFfYU7daps7Sy6OglF1x0q0w48slt0KMXbHd2/LvKVIiYwyofYCXed0yzcEOF2IYm9FjXdcn+ug==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.798.0.tgz", + "integrity": "sha512-zqWwKhhdf5CVRL6+4vNNTZVHWH9OiiwUWA3ka44jJaAMBRbbryjRedzwkWbgDaL1EbfTbcBZTYzE7N/vK7UUVA==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -363,16 +363,16 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.796.0.tgz", - "integrity": "sha512-WeNK7OWPrsOvhO3DAgpUO0FtmVghMaZ/IpPJHJ4Y0nBIsWOBXLrbZ2Y1mdT8N2bGGUaM91tJaV8Yf8COc3gvmA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.798.0.tgz", + "integrity": "sha512-Mrhl4wS4lMpuw2NCga5/rtQehNfyRs8NUHfvrLK5bZvJbjanrh8QtdRVhrAjw71OwFh3GK49QMByGkUssALJ+g==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.796.0", - "@aws-sdk/credential-provider-http": "3.796.0", - "@aws-sdk/credential-provider-ini": "3.796.0", - "@aws-sdk/credential-provider-process": "3.796.0", - "@aws-sdk/credential-provider-sso": "3.796.0", - "@aws-sdk/credential-provider-web-identity": "3.796.0", + "@aws-sdk/credential-provider-env": "3.798.0", + "@aws-sdk/credential-provider-http": "3.798.0", + "@aws-sdk/credential-provider-ini": "3.798.0", + "@aws-sdk/credential-provider-process": "3.798.0", + "@aws-sdk/credential-provider-sso": "3.798.0", + "@aws-sdk/credential-provider-web-identity": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", @@ -385,11 +385,11 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.796.0.tgz", - "integrity": "sha512-r4e8/4AdKn/qQbRVocW7oXkpoiuXdTv0qty8AASNLnbQnT1vjD1bvmP6kp4fbHPWgwY8I9h0Dqjp49uy9Bqyuw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.798.0.tgz", + "integrity": "sha512-BbRq8bhCHC94OTRIg5edgGTaWUzBH0h/IZJZ0vERle8A9nfl+5jUplvC8cvh3/8cNgHIRXj5HzlDjeSVe9dySg==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -401,13 +401,13 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.796.0.tgz", - "integrity": "sha512-RUYsQ1t6UdzkpZ7pocUt1l/9l9GCYCaopIhv0DU6CipA8rkWtoweKsLHKdv+8wE4p6gqDfDIHGam1ivswiCIzg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.798.0.tgz", + "integrity": "sha512-MLpQRb7xkqI9w0slEA76QiHGzM0PDMcpVcQG0wFHrpLKkQYjYlD9H3VfxdYGUh+FPOaR1fFpRZb18Gz9MR/2eQ==", "dependencies": { - "@aws-sdk/client-sso": "3.796.0", - "@aws-sdk/core": "3.796.0", - "@aws-sdk/token-providers": "3.796.0", + "@aws-sdk/client-sso": "3.798.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/token-providers": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -419,12 +419,12 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.796.0.tgz", - "integrity": "sha512-dpmFJT4IyjT09vruvMu/rWQQjVreqdxAe8pLPpGhoeKyA1O6+PS73b+VNXKvD31rQT8e4g6dVpA6KMxNW63aag==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.798.0.tgz", + "integrity": "sha512-OWBDy/ZiC0pxLzp1Nhah5jxDZ/onLTjouIVGPyc9E8/KzUJxqQbR6fk43VqhpYdVp/S7yDDbaOpO072RRZJQrw==", "dependencies": { - "@aws-sdk/core": "3.796.0", - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/core": "3.798.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -476,14 +476,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.796.0.tgz", - "integrity": "sha512-IeNg+3jNWT37J45opi5Jx89hGF0lOnZjiNwlMp3rKq7PlOqy8kWq5J1Gxk0W3tIkPpuf68CtBs/QFrRXWOjsZw==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.798.0.tgz", + "integrity": "sha512-nb3YvLokpu/2meKVH5hGVLNg+hz3IyFCESEJW+SpK7bW/SfaKpukGY1lqwqbf+edl+s20MRXeK/by1rvBChixQ==", "dependencies": { - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" @@ -493,43 +493,43 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.796.0.tgz", - "integrity": "sha512-jJ8a0ldWtXh/ice7nldUjTqja7KYlSYk1pwfIIvJLIqEn2SvQHK/pyCINTmmOmFAWXMKBQBeWUMxo1pPYNytzQ==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.798.0.tgz", + "integrity": "sha512-14iBJgg2Qqf74IeUY+z1nP5GIJIBZj8lv9mdpXrHlK8k+FcMXjpHg/B+JguSMhb2sbLeb5N0H8HLJGIRNALVWw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.796.0", + "@aws-sdk/core": "3.798.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.787.0", "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.796.0", + "@aws-sdk/util-user-agent-node": "3.798.0", "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.0", - "@smithy/middleware-retry": "^4.1.0", + "@smithy/middleware-endpoint": "^4.1.1", + "@smithy/middleware-retry": "^4.1.1", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.8", - "@smithy/util-defaults-mode-node": "^4.0.8", + "@smithy/util-defaults-mode-browser": "^4.0.9", + "@smithy/util-defaults-mode-node": "^4.0.9", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -557,11 +557,11 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.796.0.tgz", - "integrity": "sha512-Sxr/EqJBxOwLsXHv8C91N/Aao8Rgjn5bcpzplrTZ7wrfDrzqQfSCvjh7apCxdLYMKPBV+an75blCAd7JD4/bAg==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.798.0.tgz", + "integrity": "sha512-iYhNmHXfWLUwcMP9ldb/H+RMRLHZbBUWBgsoQqfb7sl6z24nH0qBJyL+oXHTCVBUYLP20CvUrVkcwlejDzyoRw==", "dependencies": { - "@aws-sdk/nested-clients": "3.796.0", + "@aws-sdk/nested-clients": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -621,11 +621,11 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.796.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.796.0.tgz", - "integrity": "sha512-9fQpNcHgVFitf1tbTT8V1xGRoRHSmOAWjrhevo6Tc0WoINMAKz+4JNqfVGWRE5Tmtpq0oHKo1RmvxXQQtJYciA==", + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.798.0.tgz", + "integrity": "sha512-yncgNd2inI+y5kdfn2i0oBwgCxwdtcVShNNVQ+5b/nuC1Lgjgcb+hmHAeTFMge7vhDP2Md8I+ih6bPMpK79lQQ==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.796.0", + "@aws-sdk/middleware-user-agent": "3.798.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", @@ -2062,9 +2062,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.2.0.tgz", - "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.0.tgz", + "integrity": "sha512-r6gvs5OfRq/w+9unPm7B3po4rmWaGh0CIL/OwHntGGux7+RhOOZLGuurbeMgWV6W55ZuyMTypJLeH0vn/ZRaWQ==", "dependencies": { "@smithy/middleware-serde": "^4.0.3", "@smithy/protocol-http": "^5.1.0", @@ -2160,11 +2160,11 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz", - "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.1.tgz", + "integrity": "sha512-z5RmcHxjvScL+LwEDU2mTNCOhgUs4lu5PGdF1K36IPRmUHhNFxNxgenSB7smyDiYD4vdKQ7CAZtG5cUErqib9w==", "dependencies": { - "@smithy/core": "^3.2.0", + "@smithy/core": "^3.3.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/node-config-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", @@ -2178,14 +2178,14 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz", - "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.1.tgz", + "integrity": "sha512-mBJOxn9aUYwcBUPQpKv9ifzrCn4EbhPUFguEZv3jB57YOMh0caS4P8HoLvUeNUI1nx4bIVH2SIbogbDfFI9DUA==", "dependencies": { "@smithy/node-config-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/service-error-classification": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", @@ -2352,12 +2352,12 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.0.tgz", - "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.1.tgz", + "integrity": "sha512-fbniZef60QdsBc4ZY0iyI8xbFHIiC/QRtPi66iE4ufjiE/aaz7AfUXzcWMkpO8r+QhLeNRIfmPchIG+3/QDZ6g==", "dependencies": { - "@smithy/core": "^3.2.0", - "@smithy/middleware-endpoint": "^4.1.0", + "@smithy/core": "^3.3.0", + "@smithy/middleware-endpoint": "^4.1.1", "@smithy/middleware-stack": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", @@ -2451,12 +2451,12 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz", - "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.9.tgz", + "integrity": "sha512-B8j0XsElvyhv6+5hlFf6vFV/uCSyLKcInpeXOGnOImX2mGXshE01RvPoGipTlRpIk53e6UfYj7WdDdgbVfXDZw==", "dependencies": { "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -2466,15 +2466,15 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz", - "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.9.tgz", + "integrity": "sha512-wTDU8P/zdIf9DOpV5qm64HVgGRXvqjqB/fJZTEQbrz3s79JHM/E7XkMm/876Oq+ZLHJQgnXM9QHDo29dlM62eA==", "dependencies": { "@smithy/config-resolver": "^4.1.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, diff --git a/functions/package.json b/functions/package.json index 4d2e4a9..59f0fdf 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,7 +15,7 @@ }, "main": "lib/index.js", "dependencies": { - "@aws-sdk/client-ses": "^3.796.0", + "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", diff --git a/functions/src/index.ts b/functions/src/index.ts index a0fb365..1c5b36c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; -export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; +export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; diff --git a/functions/src/payments/index.ts b/functions/src/payments/index.ts index 8d52c66..beda940 100644 --- a/functions/src/payments/index.ts +++ b/functions/src/payments/index.ts @@ -1 +1,2 @@ export * from './cashfree'; +export * from './phonepe'; diff --git a/functions/src/payments/phonepe/checkStatus.ts b/functions/src/payments/phonepe/checkStatus.ts new file mode 100644 index 0000000..df2fa36 --- /dev/null +++ b/functions/src/payments/phonepe/checkStatus.ts @@ -0,0 +1,139 @@ +import axios from "axios"; +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const checkPhonePePaymentStatus = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + await admin.auth().verifyIdToken(idToken); + + const merchantOrderId = request.query.merchantOrderId as string; + if (!merchantOrderId) { + response.status(400).json({ error: 'Missing merchant order ID' }); + return; + } + + const details = request.query.details === 'true'; + const errorContext = request.query.errorContext === 'true'; + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const queryParams = new URLSearchParams(); + if (details) queryParams.append('details', 'true'); + if (errorContext) queryParams.append('errorContext', 'true'); + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; + + const statusResponse = await axios.get( + `${apiUrl}/checkout/v2/order/${merchantOrderId}/status${queryString}`, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', merchantOrderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found', + message: `No record found for PhonePe order ID: ${merchantOrderId}` + }); + return; + } + + const orderDoc = orderQuery.docs[0]; + + await orderDoc.ref.update({ + orderStatus: statusResponse.data.state || 'UNKNOWN', + lastChecked: new Date(), + statusResponse: statusResponse.data + }); + logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + + response.json({ + success: true, + state: statusResponse.data.state, + data: statusResponse.data + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + + if (authError.response) { + logger.error('API error details:', { + status: authError.response.status, + data: authError.response.data + }); + + response.status(authError.response.status).json({ + success: false, + error: 'API error', + details: authError.response.data + }); + } else { + response.status(401).json({ + success: false, + error: 'Invalid authentication token or API error', + message: authError.message + }); + } + } + } catch (error: any) { + logger.error('PhonePe payment status check error:', error); + response.status(500).json({ + success: false, + error: 'Failed to check payment status', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/createPhonepeOrder.ts b/functions/src/payments/phonepe/createPhonepeOrder.ts new file mode 100644 index 0000000..fcad1c2 --- /dev/null +++ b/functions/src/payments/phonepe/createPhonepeOrder.ts @@ -0,0 +1,133 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request} from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; +import axios from "axios"; +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); + +export const createPhonePeOrder = onRequest({ + region: '#{SERVICES_RGN}#' + }, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + + const { + merchantOrderId, + amount, + expireAfter, + metaInfo, + paymentFlow + } = request.body; + + if (!merchantOrderId || !amount || !paymentFlow || !expireAfter) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + if (!paymentFlow.type || !paymentFlow.merchantUrls || !paymentFlow.merchantUrls.redirectUrl) { + response.status(400).json({ error: 'Invalid payment flow configuration' }); + return; + } + + const clientId = process.env.PHONEPE_CLIENT_ID; + const clientSecret = process.env.PHONEPE_CLIENT_SECRET; + const apiUrl = process.env.PHONEPE_API_URL; + const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1'; + + if (!clientId || !clientSecret || !apiUrl) { + logger.error('PhonePe credentials not configured'); + response.status(500).json({ error: 'Payment gateway configuration error' }); + return; + } + + try { + const tokenResponse = await axios.post( + `${apiUrl}/v1/oauth/token`, + { + client_id: clientId, + client_version: clientVersion, + client_secret: clientSecret, + grant_type: 'client_credentials', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const accessToken = tokenResponse.data.access_token; + + const paymentResponse = await axios.post( + `${apiUrl}/checkout/v2/pay`, + request.body, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `O-Bearer ${accessToken}`, + }, + } + ); + + try { + await admin.firestore().collection('payment_orders').doc(merchantOrderId).set({ + userId: uid, + amount: amount / 100, + orderStatus: paymentResponse.data.state || 'PENDING', + paymentGateway: 'PhonePe', + createdAt: new Date(), + merchantOrderId: merchantOrderId, + paymentUrl: paymentResponse.data.redirectUrl, + orderId: paymentResponse.data.orderId, + expireAt: new Date(paymentResponse.data.expireAt), + // rawResponse: paymentResponse.data, + metaInfo: metaInfo || {} + }); + } catch (firestoreError) { + logger.error('Error storing order in Firestore:', firestoreError); + } + + response.json({ + ...paymentResponse.data, + merchantOrderId: merchantOrderId + }); + + logger.info(`PhonePe order created: ${merchantOrderId}`); + } catch (apiError: any) { + logger.error('PhonePe API error:', apiError); + response.status(apiError.response?.status || 500).json({ + success: false, + error: 'Payment gateway error', + details: apiError.response?.data || apiError.message, + code: apiError.code + }); + } + } catch (authError) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token' + }); + } + } catch (error: any) { + logger.error('PhonePe order creation error:', error); + response.status(500).json({ + success: false, + error: 'Failed to create payment order', + details: error.message + }); + } + }); + }); diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts new file mode 100644 index 0000000..258f121 --- /dev/null +++ b/functions/src/payments/phonepe/index.ts @@ -0,0 +1,3 @@ +export { createPhonePeOrder } from './createPhonepeOrder'; +export { checkPhonePePaymentStatus } from './checkStatus'; +export { phonePeWebhook } from './webhook'; diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts new file mode 100644 index 0000000..d8696dc --- /dev/null +++ b/functions/src/payments/phonepe/webhook.ts @@ -0,0 +1,106 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getAdmin, getLogger } from "../../shared/config"; +import crypto from "crypto"; + +const admin = getAdmin(); +const logger = getLogger(); + +export const phonePeWebhook = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + try { + const authHeader = request.headers['authorization'] as string; + const username = process.env.PHONEPE_WEBHOOK_USERNAME; + const password = process.env.PHONEPE_WEBHOOK_PASSWORD; + + if (!authHeader || !username || !password) { + logger.error('Missing authorization header or webhook credentials'); + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + // Calculate expected authorization value + const credentialString = `${username}:${password}`; + const expectedAuth = crypto + .createHash('sha256') + .update(credentialString) + .digest('hex'); + + // PhonePe may send the header with a prefix like "SHA256 " or just the hash + const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); + + if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { + logger.error('Invalid webhook authorization'); + response.status(401).json({ error: 'Invalid authorization' }); + return; + } + + const { event, payload } = request.body; + + if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { + logger.error('Invalid webhook payload', request.body); + response.status(400).json({ error: 'Invalid payload' }); + return; + } + + logger.info(`Received PhonePe webhook: ${event}`, { + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + state: payload.state + }); + + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('orderId', '==', payload.orderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + const merchantOrderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', payload.merchantOrderId) + .limit(1) + .get(); + + if (merchantOrderQuery.empty) { + logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); + response.status(404).json({ + success: false, + error: 'Payment order not found' + }); + return; + } + + const orderDoc = merchantOrderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: payload.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: payload + }); + + logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); + } else { + const orderDoc = orderQuery.docs[0]; + await orderDoc.ref.update({ + orderStatus: payload.state || 'UNKNOWN', + lastUpdated: new Date(), + webhookEvent: event, + webhookData: payload + }); + + logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); + } + + response.status(200).json({ success: true }); + + } catch (error: any) { + logger.error('PhonePe webhook processing error:', error); + response.status(500).json({ + success: false, + error: 'Failed to process webhook', + details: error.message + }); + } +}); diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 752b2ab..ff92031 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,7 +9,9 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "target": "es2017" + "target": "es2017", + "types": [], + "skipLibCheck": true }, "compileOnSave": true, "include": [ diff --git a/package.json b/package.json index 5f79027..474c153 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,5 @@ "dependencies": { "@types/busboy": "^1.5.4", "busboy": "^1.6.0" - }, - "devDependencies": { - "@types/long": "^5.0.0" } } From c377b7243d15aacb219dc2d5019ac88af93fe328 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 6 May 2025 16:45:26 +0530 Subject: [PATCH 31/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 2df9af5..7862777 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -40,6 +40,12 @@ jobs: sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env + cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 0f86c468d2edc7cd3a7faeb9f764a11e5ee3cd08 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:47:06 +0530 Subject: [PATCH 32/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 7862777..90c6243 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -45,7 +45,6 @@ jobs: sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env - cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From b8b518be9ca2d8646c0c774ff25b0c20b20cf161 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:51:13 +0530 Subject: [PATCH 33/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 90c6243..2df9af5 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -40,11 +40,6 @@ jobs: sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env - sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env - sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env - sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env - sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env - sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 868ae3dea4f18f8a4b34f5e23eec2cbec6cf988d Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:53:13 +0530 Subject: [PATCH 34/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 2df9af5..7e98863 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -40,6 +40,7 @@ jobs: sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 3adf2a3aac1596883ca3ee5077c66933ddc37a91 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:56:58 +0530 Subject: [PATCH 35/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 7e98863..5e39312 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -41,6 +41,7 @@ jobs: sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 4b745d587fd49baf40cfb849251f5ebfd658827e Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 17:59:11 +0530 Subject: [PATCH 36/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 5e39312..9a6691d 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,6 +42,7 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 9b42fdcac14d6e849a056a6beeb63ff98aed2695 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:04:34 +0530 Subject: [PATCH 37/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 9a6691d..5e39312 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,7 +42,6 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env - sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From ccd0ee61bcb130318e4828cb169ca877924a8c4a Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:06:46 +0530 Subject: [PATCH 38/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 5e39312..9a6691d 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -42,6 +42,7 @@ jobs: sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 1e040212e49154e3e2e3bf54dab4e4a8b714e0ed Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Tue, 6 May 2025 18:17:09 +0530 Subject: [PATCH 39/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 9a6691d..90c6243 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -43,6 +43,8 @@ jobs: sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | From 5f89a5cda4977a7afb76af3f3aa0c9d50dd96e9b Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Wed, 7 May 2025 07:20:27 +0000 Subject: [PATCH 40/81] Email with attachment issue fixed (#25) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/25 Co-authored-by: Sharon Dcruz Co-committed-by: Sharon Dcruz --- functions/package-lock.json | 24 ++++------- functions/package.json | 4 +- functions/src/email/sendEmailSES.ts | 67 +++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 28053b5..b53c64d 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -11,14 +11,14 @@ "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", - "axios": "^1.8.4", + "axios": "^1.9.0", "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", - "long": "^4.0.0", + "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", @@ -1373,12 +1373,6 @@ "node": ">=6" } }, - "node_modules/@grpc/proto-loader/node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "optional": true - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2755,6 +2749,7 @@ "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", "dev": true, + "license": "MIT", "dependencies": { "long": "*" } @@ -3051,6 +3046,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6059,9 +6055,10 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -6687,11 +6684,6 @@ "node": ">=12.0.0" } }, - "node_modules/protobufjs/node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/functions/package.json b/functions/package.json index 59f0fdf..f83c0fd 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,14 +18,14 @@ "@aws-sdk/client-ses": "^3.798.0", "@types/node-fetch": "^2.6.12", "aws-sdk": "^2.1692.0", - "axios": "^1.8.4", + "axios": "^1.9.0", "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", - "long": "^4.0.0", + "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index a99c3b1..78e6875 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -6,6 +6,7 @@ import { SESClient } from "@aws-sdk/client-ses"; import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; import { HttpsError } from "firebase-functions/v2/https"; import * as mime from 'mime-types'; +import axios from 'axios'; const logger = getLogger(); const corsHandler = getCorsHandler(); @@ -18,6 +19,8 @@ interface EmailRequest { from: string; replyTo?: string; attachments?: Attachment[]; + fileUrl?: string; + fileName?: string; } interface Attachment { @@ -121,31 +124,71 @@ async function sendEmailWithAttachments(data: EmailRequest, recipients: string[] return { messageId: result.MessageId }; } +async function downloadFileFromUrl(url: string): Promise { + try { + const response = await axios.get(url, { responseType: 'arraybuffer' }); + return Buffer.from(response.data); + } catch (error) { + logger.error(`Error downloading file from URL: ${error}`); + throw new Error(`Failed to download file: ${error}`); + } +} + export const sendEmailSES = onRequest({ region: 'asia-south1' }, (request: Request, response) => { return corsHandler(request, response, async () => { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const data: EmailRequest = { - to: toAddress, - subject: subject, - html: message, - text: stripHtml(message), - from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', - replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', - attachments: request.body.attachments as Attachment[] || [] - }; try { + const toAddress = request.body.toAddress; + const subject = request.body.subject; + const message = request.body.message; + + // Initialize data with basic fields + const data: EmailRequest = { + to: toAddress, + html: message, + subject: subject, + text: stripHtml(message), + from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', + replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', + attachments: request.body.attachments as Attachment[] || [] + }; + + // Handle file URL if provided + if (request.body.fileUrl && request.body.fileName) { + logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); + try { + const fileContent = await downloadFileFromUrl(request.body.fileUrl); + + // If attachments array doesn't exist, create it + if (!data.attachments) { + data.attachments = []; + } + + // Add the downloaded file as an attachment + data.attachments.push({ + filename: request.body.fileName, + content: fileContent, + contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' + }); + + logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); + } catch (downloadError) { + logger.error(`Failed to download attachment: ${downloadError}`); + throw new Error(`Failed to process attachment: ${downloadError}`); + } + } + if (!data.to || !data.subject || !data.html || !data.from) { throw new HttpsError( 'invalid-argument', 'Missing required email fields' ); } + logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); const recipients = Array.isArray(data.to) ? data.to : [data.to]; + if (data.attachments && data.attachments.length > 0) { const messageResult = await sendEmailWithAttachments(data, recipients); response.status(200).json(messageResult); From 38d5092ee3283dc779d0b9bb573fa451be79206c Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 12 May 2025 13:13:27 +0530 Subject: [PATCH 41/81] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index d8696dc..e5d87ab 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -10,6 +10,13 @@ export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { + + logger.info('Received webhook request', { + headers: request.headers, + body: request.body, + method: request.method + }); + const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; From 6d64f1e4d73341b77bb0df73614a57902d146be2 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 14 May 2025 18:34:58 +0530 Subject: [PATCH 42/81] Logging and client profile change --- functions/src/clientRegistration/clientRegistration.ts | 2 +- functions/src/payments/phonepe/webhook.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index 8ed51fb..fe2fafc 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -82,7 +82,7 @@ export const registerClient = onRequest({ phoneNumber: formattedPhoneNumber, }; - await admin.firestore().collection('client_profile').doc(clientUid).set(clientData); + await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); return res.status(201).json({ success: true, diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index e5d87ab..9f522f9 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -27,14 +27,12 @@ export const phonePeWebhook = onRequest({ return; } - // Calculate expected authorization value const credentialString = `${username}:${password}`; const expectedAuth = crypto .createHash('sha256') .update(credentialString) .digest('hex'); - // PhonePe may send the header with a prefix like "SHA256 " or just the hash const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { From aee28a60500546000f15dda50d021fac1ac3b6d3 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 14 May 2025 13:09:09 +0000 Subject: [PATCH 43/81] phonepe (#26) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/26 --- functions/src/clientRegistration/clientRegistration.ts | 2 +- functions/src/payments/phonepe/webhook.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index 8ed51fb..fe2fafc 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -82,7 +82,7 @@ export const registerClient = onRequest({ phoneNumber: formattedPhoneNumber, }; - await admin.firestore().collection('client_profile').doc(clientUid).set(clientData); + await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); return res.status(201).json({ success: true, diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index d8696dc..9f522f9 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -10,6 +10,13 @@ export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { + + logger.info('Received webhook request', { + headers: request.headers, + body: request.body, + method: request.method + }); + const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; @@ -20,14 +27,12 @@ export const phonePeWebhook = onRequest({ return; } - // Calculate expected authorization value const credentialString = `${username}:${password}`; const expectedAuth = crypto .createHash('sha256') .update(credentialString) .digest('hex'); - // PhonePe may send the header with a prefix like "SHA256 " or just the hash const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { From e8710074c41a1e047b7eaa5a81f3dd4efd8829a4 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 19 May 2025 12:52:06 +0530 Subject: [PATCH 44/81] Invoice --- firebase.json | 2 +- functions/src/index.ts | 2 +- functions/src/payments/phonepe/checkStatus.ts | 146 +++++++- functions/src/payments/phonepe/index.ts | 2 + .../payments/phonepe/invoice/directInvoice.ts | 106 ++++++ .../payments/phonepe/invoice/getInvoiceUrl.ts | 63 ++++ .../src/payments/phonepe/invoice/index.ts | 12 + .../phonepe/invoice/invoiceService.ts | 327 +++++++++++++++++ .../phonepe/invoice/processInvoice.ts | 83 +++++ .../phonepe/invoice/sendInvoiceEmail.ts | 91 +++++ functions/src/payments/phonepe/paymentData.ts | 130 +++++++ functions/src/payments/phonepe/webhook.ts | 338 +++++++++++++++++- functions/src/utils/emailService.ts | 70 ++++ package-lock.json | 208 ++++++++++- package.json | 7 +- 15 files changed, 1533 insertions(+), 54 deletions(-) create mode 100644 functions/src/payments/phonepe/invoice/directInvoice.ts create mode 100644 functions/src/payments/phonepe/invoice/getInvoiceUrl.ts create mode 100644 functions/src/payments/phonepe/invoice/index.ts create mode 100644 functions/src/payments/phonepe/invoice/invoiceService.ts create mode 100644 functions/src/payments/phonepe/invoice/processInvoice.ts create mode 100644 functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts create mode 100644 functions/src/payments/phonepe/paymentData.ts create mode 100644 functions/src/utils/emailService.ts diff --git a/firebase.json b/firebase.json index d8682a0..dfc4226 100644 --- a/firebase.json +++ b/firebase.json @@ -25,7 +25,7 @@ "port": 5005 }, "firestore": { - "port": 8085 + "port": 8086 }, "storage": { "port": 9199 diff --git a/functions/src/index.ts b/functions/src/index.ts index 1c5b36c..19fa4e0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; -export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments'; +export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; diff --git a/functions/src/payments/phonepe/checkStatus.ts b/functions/src/payments/phonepe/checkStatus.ts index df2fa36..22a9c25 100644 --- a/functions/src/payments/phonepe/checkStatus.ts +++ b/functions/src/payments/phonepe/checkStatus.ts @@ -3,9 +3,13 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request} from "firebase-functions/v2/https"; import { getCorsHandler } from "../../shared/middleware"; import { getAdmin, getLogger } from "../../shared/config"; +import { updatePaymentDataAfterSuccess } from "./paymentData"; +import { InvoiceService } from "./invoice/invoiceService"; + const admin = getAdmin(); const logger = getLogger(); const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); export const checkPhonePePaymentStatus = onRequest({ region: '#{SERVICES_RGN}#' @@ -80,30 +84,144 @@ export const checkPhonePePaymentStatus = onRequest({ .limit(1) .get(); - if (orderQuery.empty) { + if (orderQuery.empty) { logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`); response.status(404).json({ - success: false, - error: 'Payment order not found', - message: `No record found for PhonePe order ID: ${merchantOrderId}` + success: false, + error: 'Payment order not found', + message: `No record found for PhonePe order ID: ${merchantOrderId}` }); return; - } + } - const orderDoc = orderQuery.docs[0]; + const orderDoc = orderQuery.docs[0]; + const orderData = orderDoc.data(); - await orderDoc.ref.update({ + await orderDoc.ref.update({ orderStatus: statusResponse.data.state || 'UNKNOWN', lastChecked: new Date(), statusResponse: statusResponse.data - }); - logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + }); + + if (statusResponse.data.state === 'COMPLETED') { + try { + // Update payment data + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( + merchantOrderId, + statusResponse.data.orderId, + statusResponse.data + ); + + if (paymentUpdateSuccess) { + // Extract membership ID from metaInfo + const membershipId = orderData.metaInfo?.membershipId; + + if (membershipId) { + try { + // Get user data for invoice + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (membershipDoc.exists) { + const membershipData = membershipDoc.data(); + const userId = membershipData?.userId; + + // Get user details + const userDoc = await admin.firestore() + .collection('users') + .doc(userId) + .get(); + + if (userDoc.exists) { + const userData = userDoc.data(); + + // Get gym details + const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; + let gymName = 'Fitlien'; + let gymAddress = ''; + let gstNumber = ''; + + if (gymId) { + const gymDoc = await admin.firestore() + .collection('gyms') + .doc(gymId) + .get(); + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + gstNumber = gymData?.gstNumber || ''; + } + } + + // Generate invoice data + const invoiceData = { + invoiceNumber: `INV-${merchantOrderId.substring(0, 8)}`, + businessName: gymName, + address: gymAddress, + gstNumber: gstNumber, + customerName: userData?.displayName || `${membershipData?.['first-name'] || ''} ${membershipData?.['last-name'] || ''}`.trim(), + phoneNumber: membershipData?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.['email'] || '', + planName: orderData.metaInfo?.planName || 'Membership', + amount: orderData.amount, + transactionId: statusResponse.data.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + + // Update payment record with invoice path + await admin.firestore() + .collection('membership_payments') + .doc(membershipId) + .get() + .then(async (doc) => { + if (doc.exists) { + const paymentsData = doc.data()?.payments || []; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === merchantOrderId || + paymentsData[i].transactionId === statusResponse.data.orderId) { + paymentsData[i].invoicePath = invoicePath; + break; + } + } + + await doc.ref.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + } + }); + + logger.info(`Generated invoice for payment: ${merchantOrderId}, path: ${invoicePath}`); + } + } + } catch (invoiceError) { + logger.error('Error generating invoice:', invoiceError); + // Continue processing - don't fail the response + } + } + } + + logger.info(`Payment data updated for completed payment: ${merchantOrderId}`); + } catch (paymentUpdateError) { + logger.error('Error updating payment data:', paymentUpdateError); + // Continue processing - don't fail the response + } + } - response.json({ - success: true, - state: statusResponse.data.state, - data: statusResponse.data - }); + logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + + response.json({ + success: true, + state: statusResponse.data.state, + data: statusResponse.data + }); } catch (authError: any) { logger.error('Authentication error:', authError); diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts index 258f121..050d5b2 100644 --- a/functions/src/payments/phonepe/index.ts +++ b/functions/src/payments/phonepe/index.ts @@ -1,3 +1,5 @@ export { createPhonePeOrder } from './createPhonepeOrder'; export { checkPhonePePaymentStatus } from './checkStatus'; export { phonePeWebhook } from './webhook'; +export { updatePaymentDataAfterSuccess } from './paymentData'; +export * from './invoice'; diff --git a/functions/src/payments/phonepe/invoice/directInvoice.ts b/functions/src/payments/phonepe/invoice/directInvoice.ts new file mode 100644 index 0000000..93969f5 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/directInvoice.ts @@ -0,0 +1,106 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService, InvoiceData } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const directGenerateInvoice = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + invoiceNumber, + businessName, + address, + gstNumber, + customerName, + phoneNumber, + email, + planName, + amount, + transactionId, + paymentDate, + paymentMethod, + sendEmail, + emailOptions + } = request.body; + + if (!invoiceNumber || !businessName || !customerName || !amount || !transactionId) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const invoiceData: InvoiceData = { + invoiceNumber, + businessName, + address: address || '', + gstNumber, + customerName, + phoneNumber: phoneNumber || '', + email: email || '', + planName: planName || 'Membership', + amount: parseFloat(amount), + transactionId, + paymentDate: paymentDate ? new Date(paymentDate) : new Date(), + paymentMethod: paymentMethod || 'Online' + }; + + // Generate the invoice without updating any payment records + const invoicePath = await invoiceService.generateInvoice(invoiceData); + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + // Send email if requested + let emailSent = false; + if (sendEmail && email) { + emailSent = await invoiceService.sendInvoiceEmail(invoicePath, { + recipientEmail: email, + recipientName: customerName, + ...emailOptions + }); + } + + response.json({ + success: true, + invoicePath, + downloadUrl, + emailSent + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error generating invoice:', error); + response.status(500).json({ + success: false, + error: 'Failed to generate invoice', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts new file mode 100644 index 0000000..038a14c --- /dev/null +++ b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts @@ -0,0 +1,63 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const getInvoiceUrl = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { invoicePath } = request.query; + + if (!invoicePath) { + response.status(400).json({ + success: false, + error: 'Missing invoice path' + }); + return; + } + + // Get a download URL for the invoice + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); + + response.json({ + success: true, + downloadUrl + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error getting invoice URL:', error); + response.status(500).json({ + success: false, + error: 'Failed to get invoice URL', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/index.ts b/functions/src/payments/phonepe/invoice/index.ts new file mode 100644 index 0000000..3d423aa --- /dev/null +++ b/functions/src/payments/phonepe/invoice/index.ts @@ -0,0 +1,12 @@ +import { getInvoiceUrl } from './getInvoiceUrl'; +import { InvoiceService } from './invoiceService'; +import { processInvoice } from './processInvoice'; +import { sendInvoiceEmail } from './sendInvoiceEmail'; + +// Export all invoice-related functions +export { + getInvoiceUrl, + InvoiceService, + processInvoice, + sendInvoiceEmail +}; diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts new file mode 100644 index 0000000..032b259 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -0,0 +1,327 @@ +import { getAdmin, getLogger } from "../../../shared/config"; +import PDFDocument from 'pdfkit'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { format } from 'date-fns'; +import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; + +const admin = getAdmin(); +const logger = getLogger(); + +export interface InvoiceData { + invoiceNumber: string; + businessName: string; + address: string; + gstNumber?: string; + customerName: string; + phoneNumber: string; + email: string; + planName: string; + amount: number; + transactionId: string; + paymentDate: Date; + paymentMethod: string; +} + +export interface EmailOptions { + recipientEmail: string; + recipientName?: string; + subject?: string; + customHtml?: string; + additionalData?: { + gymName?: string; + planName?: string; + amount?: number; + transactionId?: string; + paymentDate?: Date; + paymentMethod?: string; + }; +} + +export class InvoiceService { + async generateInvoice(data: InvoiceData): Promise { + try { + const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); + const doc = new PDFDocument({ margin: 50 }); + + // Create a write stream to the temporary file + const writeStream = fs.createWriteStream(tempFilePath); + doc.pipe(writeStream); + + // Check if GST is applicable + const hasGst = data.gstNumber && data.gstNumber.length > 0; + const baseAmount = hasGst ? data.amount / 1.18 : data.amount; + const sgst = hasGst ? baseAmount * 0.09 : 0; + const cgst = hasGst ? baseAmount * 0.09 : 0; + + // Add business details + doc.fontSize(20).font('Helvetica-Bold').text(data.businessName, 50, 50); + doc.fontSize(12).font('Helvetica').text(data.address, 50, 75, { width: 250 }); + if (hasGst) { + doc.text(`GSTIN: ${data.gstNumber}`, 50, 115); + } + + // Add invoice title and details + doc.fontSize(24).font('Helvetica-Bold').text('RECEIPT', 400, 50, { align: 'right' }); + doc.fontSize(12).font('Helvetica').text(`Receipt #: ${data.invoiceNumber}`, 400, 80, { align: 'right' }); + doc.text(`Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 400, 95, { align: 'right' }); + + // Add customer details + doc.rect(50, 150, 500, 80).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('Receipt To:', 60, 160); + doc.fontSize(12).font('Helvetica').text(data.customerName, 60, 175); + doc.text(`Phone: ${data.phoneNumber}`, 60, 190); + doc.text(`Email: ${data.email}`, 60, 205); + + // Add table header + const tableTop = 260; + doc.rect(50, tableTop, 500, 30).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('No.', 60, tableTop + 10, { width: 30, align: 'center' }); + doc.text('Description', 100, tableTop + 10, { width: 250 }); + doc.text('HSN/SAC', 350, tableTop + 10, { width: 80, align: 'center' }); + doc.text('Amount (INR)', 430, tableTop + 10, { width: 100, align: 'right' }); + + // Add table row + const rowTop = tableTop + 30; + doc.rect(50, rowTop, 500, 30).stroke(); + doc.fontSize(12).font('Helvetica').text('1', 60, rowTop + 10, { width: 30, align: 'center' }); + doc.text(`${data.planName} Subscription`, 100, rowTop + 10, { width: 250 }); + doc.text('999723', 350, rowTop + 10, { width: 80, align: 'center' }); + doc.text(baseAmount.toFixed(2), 430, rowTop + 10, { width: 100, align: 'right' }); + + // Add totals + let currentY = rowTop + 50; + + if (hasGst) { + doc.fontSize(12).font('Helvetica').text('Taxable Amount:', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + + doc.text('SGST (9%):', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${sgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + + doc.text('CGST (9%):', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${cgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + } + + doc.moveTo(350, currentY).lineTo(550, currentY).stroke(); + currentY += 10; + + doc.fontSize(12).font('Helvetica-Bold').text('Total Amount:', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + + // Add payment information + currentY += 40; + doc.rect(50, currentY, 500, 80).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('Payment Information:', 60, currentY + 10); + doc.fontSize(12).font('Helvetica').text(`Transaction ID: ${data.transactionId}`, 60, currentY + 30); + doc.text(`Payment Method: ${data.paymentMethod}`, 60, currentY + 45); + doc.text(`Payment Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 60, currentY + 60); + + // Add footer + currentY += 100; + doc.fontSize(12).font('Helvetica-Oblique').text('Thank you for your business!', 50, currentY, { align: 'center' }); + doc.fontSize(10).font('Helvetica').text('This is a computer-generated receipt and does not require a signature.', 50, currentY + 20, { align: 'center' }); + + // Finalize the PDF + doc.end(); + + // Wait for the file to be written + await new Promise((resolve, reject) => { + writeStream.on('finish', () => resolve()); + writeStream.on('error', reject); + }); + + // Upload to Firebase Storage using admin SDK + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; + const bucket = admin.storage().bucket(); + await bucket.upload(tempFilePath, { + destination: invoicePath, + metadata: { + contentType: 'application/pdf', + }, + }); + + // Clean up the temporary file + fs.unlinkSync(tempFilePath); + + // Return the storage path + return invoicePath; + } catch (error: any) { + logger.error('Error generating invoice:', error); + throw new Error(`Failed to generate invoice: ${error.message}`); + } + } + + async getInvoiceDownloadUrl(invoicePath: string): Promise { + try { + // Using admin SDK to generate a signed URL + const bucket = admin.storage().bucket(); + const file = bucket.file(invoicePath); + + const expirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days + + const [signedUrl] = await file.getSignedUrl({ + action: 'read', + expires: Date.now() + expirationMs, + responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`, + }); + + return signedUrl; + } catch (error: any) { + logger.error('Error getting invoice download URL:', error); + throw new Error(`Failed to get invoice download URL: ${error.message}`); + } + } + + async updateInvoicePath(membershipId: string, paymentId: string, invoicePath: string): Promise { + try { + const membershipPaymentsRef = admin.firestore() + .collection('membership_payments') + .doc(membershipId); + + const docSnapshot = await membershipPaymentsRef.get(); + + if (!docSnapshot.exists) { + logger.error(`No membership payments found for membershipId: ${membershipId}`); + return false; + } + + const data = docSnapshot.data(); + const paymentsData = data?.payments || []; + + // Find the payment by referenceNumber or transactionId + let found = false; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === paymentId || + paymentsData[i].transactionId === paymentId) { + paymentsData[i].invoicePath = invoicePath; + found = true; + break; + } + } + + if (!found) { + logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`); + return false; + } + + await membershipPaymentsRef.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + + logger.info(`Successfully updated invoice path for payment: ${paymentId}`); + return true; + } catch (error: any) { + logger.error('Error updating payment with invoice path:', error); + return false; + } + } + + async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { + try { + // Get the download URL for the invoice + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); + + // Format the date + const formattedDate = emailOptions.additionalData?.paymentDate + ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') + : new Date().toLocaleDateString('en-GB'); + + // Create email HTML content if not provided + const emailHtml = emailOptions.customHtml || ` + + +

Thank you for your payment

+

Dear ${emailOptions.recipientName || 'Valued Customer'},

+

Thank you for your payment. Your membership has been successfully activated.

+

Please find attached your invoice for the payment.

+

Membership Details:

+
    +
  • Gym: ${emailOptions.additionalData?.gymName || 'Fitlien'}
  • +
  • Plan: ${emailOptions.additionalData?.planName || 'Membership'}
  • +
  • Amount: ₹${emailOptions.additionalData?.amount || '0'}
  • +
  • Transaction ID: ${emailOptions.additionalData?.transactionId || 'N/A'}
  • +
  • Date: ${formattedDate}
  • +
  • Payment Method: ${emailOptions.additionalData?.paymentMethod || 'Online'}
  • +
+

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + // Send the email with attachment + await sendEmailWithAttachmentUtil( + emailOptions.recipientEmail, + emailOptions.subject || 'Your Fitlien Membership Invoice', + emailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`); + return true; + } catch (error: any) { + logger.error('Error sending invoice email:', error); + return false; + } + } + + async processInvoice( + membershipId: string, + paymentId: string, + invoiceData: InvoiceData, + emailOptions?: EmailOptions + ): Promise<{ + success: boolean; + invoicePath?: string; + downloadUrl?: string; + emailSent: boolean; + error?: string; + }> { + try { + // Generate the invoice + const invoicePath = await this.generateInvoice(invoiceData); + + // Update the payment record with the invoice path + const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); + + if (!updateSuccess) { + return { + success: false, + invoicePath, + emailSent: false, + error: 'Failed to update payment with invoice path' + }; + } + + // Get a download URL for the invoice + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); + + // Send email if email options are provided + let emailSent = false; + if (emailOptions && emailOptions.recipientEmail) { + emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); + } + + return { + success: true, + invoicePath, + downloadUrl, + emailSent + }; + } catch (error: any) { + logger.error('Error processing invoice:', error); + return { + success: false, + emailSent: false, + error: error.message + }; + } + } +} diff --git a/functions/src/payments/phonepe/invoice/processInvoice.ts b/functions/src/payments/phonepe/invoice/processInvoice.ts new file mode 100644 index 0000000..1164d89 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/processInvoice.ts @@ -0,0 +1,83 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const processInvoice = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + membershipId, + paymentId, + invoiceData, + emailOptions + } = request.body; + + if (!membershipId || !paymentId || !invoiceData) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const result = await invoiceService.processInvoice( + membershipId, + paymentId, + invoiceData, + emailOptions + ); + + if (!result.success) { + response.status(400).json({ + success: false, + error: result.error || 'Failed to process invoice' + }); + return; + } + + response.json({ + success: true, + message: 'Invoice processed successfully', + invoicePath: result.invoicePath, + downloadUrl: result.downloadUrl, + emailSent: result.emailSent + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error processing invoice:', error); + response.status(500).json({ + success: false, + error: 'Failed to process invoice', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts b/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts new file mode 100644 index 0000000..2af677a --- /dev/null +++ b/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts @@ -0,0 +1,91 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService, EmailOptions } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const sendInvoiceEmail = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + invoicePath, + recipientEmail, + recipientName, + subject, + customHtml, + gymName, + planName, + amount, + transactionId, + paymentDate, + paymentMethod + } = request.body; + + if (!invoicePath || !recipientEmail) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const emailOptions: EmailOptions = { + recipientEmail, + recipientName, + subject, + customHtml, + additionalData: { + gymName, + planName, + amount, + transactionId, + paymentDate: paymentDate ? new Date(paymentDate) : undefined, + paymentMethod + } + }; + + const emailSent = await invoiceService.sendInvoiceEmail(invoicePath, emailOptions); + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + response.json({ + success: true, + message: emailSent ? 'Invoice email sent successfully' : 'Failed to send email but URL generated', + downloadUrl + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error sending invoice email:', error); + response.status(500).json({ + success: false, + error: 'Failed to send invoice email', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/paymentData.ts b/functions/src/payments/phonepe/paymentData.ts new file mode 100644 index 0000000..b6eca48 --- /dev/null +++ b/functions/src/payments/phonepe/paymentData.ts @@ -0,0 +1,130 @@ +import { getAdmin, getLogger } from "../../shared/config"; + +const admin = getAdmin(); +const logger = getLogger(); + +// Define an interface for the payment data to avoid type errors +interface PaymentData { + id: string; + date: string; + dateTimestamp: FirebaseFirestore.Timestamp; + amount: any; + paymentMethod: string; + referenceNumber: string; + discount: any; + transactionId: string; + createdAt: Date; + invoicePath?: string; // Make this optional +} + +export async function updatePaymentDataAfterSuccess( + merchantOrderId: string, + orderId: string, + paymentDetails: any, + invoicePath?: string +): Promise { + try { + // Get the payment order from Firestore + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', merchantOrderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + logger.error(`No payment order found with merchantOrderId: ${merchantOrderId}`); + return false; + } + + const orderDoc = orderQuery.docs[0]; + const orderData = orderDoc.data(); + + // Extract membership ID from metaInfo + const membershipId = orderData.metaInfo?.membershipId; + if (!membershipId) { + logger.error(`No membershipId found in metaInfo for order: ${merchantOrderId}`); + return false; + } + + const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); // DD-MM-YYYY format + const dateTimestamp = admin.firestore.Timestamp.now(); + + // Create payment data object with proper typing + const paymentData: PaymentData = { + id: admin.firestore().collection('_').doc().id, // Generate a UUID + date: isoDate, + dateTimestamp: dateTimestamp, + amount: orderData.amount, + paymentMethod: 'Online', + referenceNumber: merchantOrderId, + discount: orderData.metaInfo?.discount || null, + transactionId: orderId, + createdAt: new Date() + }; + + // Add invoice path if provided + if (invoicePath) { + paymentData.invoicePath = invoicePath; + } + + // Get reference to membership payments document + const membershipPaymentsRef = admin.firestore() + .collection('membership_payments') + .doc(membershipId); + + const docSnapshot = await membershipPaymentsRef.get(); + + // Update or create the membership payments document + if (docSnapshot.exists) { + await membershipPaymentsRef.update({ + 'payments': admin.firestore.FieldValue.arrayUnion(paymentData), + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + 'updatedBy': orderData.userId, + }); + } else { + await membershipPaymentsRef.set({ + 'membershipId': membershipId, + 'payments': [paymentData], + 'createdAt': admin.firestore.FieldValue.serverTimestamp(), + 'createdBy': orderData.userId, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + 'updatedBy': orderData.userId, + }); + } + + // Update membership status + await updateMembershipStatus(membershipId, orderData.userId); + + logger.info(`Successfully updated payment data for membership: ${membershipId}`); + return true; + } catch (error: any) { + logger.error('Error updating payment data:', error); + return false; + } +} + +async function updateMembershipStatus(membershipId: string, userId: string): Promise { + try { + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (!membershipDoc.exists) { + throw new Error(`Membership not found for id: ${membershipId}`); + } + + await admin.firestore() + .collection('memberships') + .doc(membershipId) + .update({ + 'status': 'ACTIVE', // Assuming this matches your InvitationStatus.active + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + + logger.info(`Successfully updated membership status for: ${membershipId}`); + } catch (error: any) { + logger.error('Error updating membership status:', error); + throw error; + } +} diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9f522f9..fef3bbb 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -2,25 +2,30 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; import { getAdmin, getLogger } from "../../shared/config"; import crypto from "crypto"; +import { updatePaymentDataAfterSuccess } from "./paymentData"; +import { InvoiceService } from "./invoice/invoiceService"; +import * as path from 'path'; +import { sendEmailWithAttachmentUtil } from "../../utils/emailService"; +import { format } from 'date-fns'; const admin = getAdmin(); const logger = getLogger(); +const invoiceService = new InvoiceService(); export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { - logger.info('Received webhook request', { headers: request.headers, body: request.body, method: request.method }); - + const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; - + if (!authHeader || !username || !password) { logger.error('Missing authorization header or webhook credentials'); response.status(401).json({ error: 'Unauthorized' }); @@ -32,9 +37,9 @@ export const phonePeWebhook = onRequest({ .createHash('sha256') .update(credentialString) .digest('hex'); - + const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); - + if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { logger.error('Invalid webhook authorization'); response.status(401).json({ error: 'Invalid authorization' }); @@ -42,32 +47,34 @@ export const phonePeWebhook = onRequest({ } const { event, payload } = request.body; - + if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { logger.error('Invalid webhook payload', request.body); response.status(400).json({ error: 'Invalid payload' }); return; } - - logger.info(`Received PhonePe webhook: ${event}`, { + + logger.info(`Received PhonePe webhook: ${event}`, { merchantOrderId: payload.merchantOrderId, orderId: payload.orderId, state: payload.state }); - + const orderQuery = await admin.firestore() .collection('payment_orders') .where('orderId', '==', payload.orderId) .limit(1) .get(); - + + let orderDoc; + if (orderQuery.empty) { const merchantOrderQuery = await admin.firestore() .collection('payment_orders') .where('merchantOrderId', '==', payload.merchantOrderId) .limit(1) .get(); - + if (merchantOrderQuery.empty) { logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); response.status(404).json({ @@ -76,30 +83,325 @@ export const phonePeWebhook = onRequest({ }); return; } - - const orderDoc = merchantOrderQuery.docs[0]; + + orderDoc = merchantOrderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); - + logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); } else { - const orderDoc = orderQuery.docs[0]; + orderDoc = orderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); - + logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } - + + if (payload.state === 'COMPLETED') { + try { + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( + payload.merchantOrderId, + payload.orderId, + payload + ); + + if (paymentUpdateSuccess) { + const orderData = orderDoc.data(); + + const membershipId = orderData.metaInfo?.membershipId; + + if (membershipId) { + try { + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (membershipDoc.exists) { + const membershipData = membershipDoc.data(); + const userId = membershipData?.userId; + + const userDoc = await admin.firestore() + .collection('users') + .doc(userId) + .get(); + + if (userDoc.exists) { + const userData = userDoc.data(); + + const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; + let gymName = 'Fitlien'; + let gymAddress = ''; + let subscriptionName = ''; + let gymOwnerEmail = ''; + let gymPhoneNumber = ''; + let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; + let trainerId = orderData.metaInfo?.trainerId; + let trainerData = null; + + const discountPercentage = orderData.metaInfo?.discount || 0; + const hasDiscount = discountPercentage > 0; + const isFreeplan = discountPercentage === 100; + const originalAmount = hasDiscount ? + orderData.amount / (1 - discountPercentage / 100) : + orderData.amount; + const discountText = isFreeplan ? + " (Free Plan)" : + hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` : + ''; + const amountSaved = hasDiscount ? + originalAmount - orderData.amount : + 0; + + if (gymId) { + const gymDoc = await admin.firestore() + .collection('gyms') + .doc(gymId) + .get(); + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + subscriptionName = gymData?.subscriptions?.name || ''; + gymPhoneNumber = gymData?.phoneNumber || ''; + + if (gymData?.userId) { + const gymOwnerDoc = await admin.firestore() + .collection('users') + .doc(gymData.userId) + .get(); + + if (gymOwnerDoc.exists) { + const gymOwnerData = gymOwnerDoc.data(); + gymOwnerEmail = gymOwnerData?.email || ''; + } + } + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerId) { + try { + const trainerDoc = await admin.firestore() + .collection('trainer_profiles') + .doc(trainerId) + .get(); + + if (trainerDoc.exists) { + trainerData = trainerDoc.data(); + } + } catch (trainerError) { + logger.error('Error fetching trainer data:', trainerError); + } + } + + const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; + const invoiceData = { + invoiceNumber, + businessName: gymName, + address: gymAddress, + gstNumber: userData?.gstNumber, + customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), + phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.fields?.['email'] || '', + planName: orderData.metaInfo?.planName || subscriptionName || paymentType, + amount: orderData.amount, + transactionId: payload.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + + await admin.firestore() + .collection('membership_payments') + .doc(membershipId) + .get() + .then(async (doc) => { + if (doc.exists) { + const paymentsData = doc.data()?.payments || []; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === payload.merchantOrderId || + paymentsData[i].transactionId === payload.orderId) { + paymentsData[i].invoicePath = invoicePath; + break; + } + } + + await doc.ref.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + } + }); + + logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`); + + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + const formattedDate = format(new Date(), 'dd/MM/yyyy'); + + if (membershipData?.fields?.['email']) { + try { + const emailSubject = isFreeplan + ? `Free Plan Assigned - ${gymName}` + : `New Membership - ${gymName}`; + + const customerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}

+

Dear ${invoiceData.customerName},

+

${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}

+

Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.

+

Membership Details:

+
    +
  • Gym: ${gymName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Your Personal Trainer'}
  • ` : ''} +
  • Plan: ${invoiceData.planName}
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • You Save: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • + ${isFreeplan ? '
  • Payment Method: Online}
  • ' : ''} +
+

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + membershipData?.fields?.['email'], + emailSubject, + customerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to ${membershipData?.fields?.['email']} for payment: ${payload.merchantOrderId}`); + } catch (emailError) { + logger.error('Error sending customer invoice email:', emailError); + } + } + + if (gymOwnerEmail) { + try { + const ownerEmailSubject = isFreeplan + ? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}` + : `New Membership${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`; + + const gymOwnerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}

+

Dear Gym Owner,

+

${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.

+

Customer Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: ${invoiceData.planName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Personal Trainer'}
  • ` : ''} + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • Amount Saved by Customer: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + gymOwnerEmail, + ownerEmailSubject, + gymOwnerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); + } catch (ownerEmailError) { + logger.error('Error sending gym owner invoice email:', ownerEmailError); + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerData && trainerData.email) { + try { + const trainerEmailHtml = ` + + +

New Personal Training Client

+

Dear ${trainerData.fullName || 'Trainer'},

+

A new client has signed up for personal training with you at ${gymName}.

+

Client Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: Personal Training Membership
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + trainerData.email, + `New Personal Training Client - ${gymName}`, + trainerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to trainer (${trainerData.email}) for payment: ${payload.merchantOrderId}`); + } catch (trainerEmailError) { + logger.error('Error sending trainer invoice email:', trainerEmailError); + } + } + } + } + } catch (invoiceError) { + logger.error('Error generating invoice:', invoiceError); + } + } + } + + logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`); + } catch (paymentUpdateError) { + logger.error('Error updating payment data:', paymentUpdateError); + } + } + response.status(200).json({ success: true }); - + } catch (error: any) { logger.error('PhonePe webhook processing error:', error); response.status(500).json({ diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts new file mode 100644 index 0000000..b7f70a8 --- /dev/null +++ b/functions/src/utils/emailService.ts @@ -0,0 +1,70 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as https from 'https'; +import { getLogger } from "../shared/config"; +import formData from 'form-data'; +import Mailgun from 'mailgun.js'; +const { convert } = require('html-to-text'); + +const mailgun = new Mailgun(formData); +const logger = getLogger(); + +export async function sendEmailWithAttachmentUtil( + toAddress: string, + subject: string, + message: string, + fileUrl: string, + fileName?: string +): Promise { + try { + const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); + await new Promise((resolve, reject) => { + const file = fs.createWriteStream(tempFilePath); + https.get(fileUrl, (res) => { + res.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', (err) => { + fs.unlink(tempFilePath, () => {}); + reject(err); + }); + }); + + try { + const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); + const options = { + wordwrap: 130, + }; + const textMessage = convert(message, options); + const fileBuffer = fs.readFileSync(tempFilePath); + const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); + + const data = { + from: process.env.MAILGUN_FROM_ADDRESS, + to: toAddress, + subject: subject, + text: textMessage, + html: message, + attachment: { + data: fileBuffer, + filename: attachmentFilename, + contentType: 'application/pdf', + } + }; + + const result = await client.messages.create(process.env.MAILGUN_SERVER!, data); + fs.unlinkSync(tempFilePath); + logger.info('Email with attachment from URL sent successfully'); + return { success: true, result }; + } catch (e) { + logger.error(`Error while sending E-mail. Error: ${e}`); + throw e; + } + } catch (error) { + logger.error('Error sending email with attachment from URL:', error); + throw error; + } +} diff --git a/package-lock.json b/package-lock.json index 6c1f8fe..b0bc379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,20 @@ "": { "dependencies": { "@types/busboy": "^1.5.4", - "busboy": "^1.6.0" - }, - "devDependencies": { - "@types/long": "^5.0.0" + "@types/nodemailer": "^6.4.17", + "@types/pdfkit": "^0.13.9", + "busboy": "^1.6.0", + "date-fns": "^4.1.0", + "nodemailer": "^7.0.3", + "pdfkit": "^0.17.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": { + "tslib": "^2.8.0" } }, "node_modules/@types/busboy": { @@ -20,16 +30,6 @@ "@types/node": "*" } }, - "node_modules/@types/long": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", - "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", - "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "long": "*" - } - }, "node_modules/@types/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", @@ -38,6 +38,49 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfkit": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz", + "integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -49,11 +92,110 @@ "node": ">=10.16.0" } }, - "node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", - "dev": true + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==" + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/pdfkit": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz", + "integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==" }, "node_modules/streamsearch": { "version": "1.1.0", @@ -63,10 +205,38 @@ "node": ">=10.0.0" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } } } } diff --git a/package.json b/package.json index 474c153..073e12e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,11 @@ { "dependencies": { "@types/busboy": "^1.5.4", - "busboy": "^1.6.0" + "@types/nodemailer": "^6.4.17", + "@types/pdfkit": "^0.13.9", + "busboy": "^1.6.0", + "date-fns": "^4.1.0", + "nodemailer": "^7.0.3", + "pdfkit": "^0.17.1" } } From b75cb415e7bc544bd29fcf7f6c89f3b125b8e142 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Mon, 19 May 2025 07:26:12 +0000 Subject: [PATCH 45/81] phonepe (#27) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/27 --- .gitea/workflows/deploy-dev.yaml | 1 + firebase.json | 2 +- functions/src/index.ts | 2 +- functions/src/payments/phonepe/checkStatus.ts | 146 +++++++- functions/src/payments/phonepe/index.ts | 2 + .../payments/phonepe/invoice/directInvoice.ts | 106 ++++++ .../payments/phonepe/invoice/getInvoiceUrl.ts | 63 ++++ .../src/payments/phonepe/invoice/index.ts | 12 + .../phonepe/invoice/invoiceService.ts | 327 +++++++++++++++++ .../phonepe/invoice/processInvoice.ts | 83 +++++ .../phonepe/invoice/sendInvoiceEmail.ts | 91 +++++ functions/src/payments/phonepe/paymentData.ts | 130 +++++++ functions/src/payments/phonepe/webhook.ts | 334 +++++++++++++++++- functions/src/utils/emailService.ts | 70 ++++ package-lock.json | 208 ++++++++++- package.json | 7 +- 16 files changed, 1532 insertions(+), 52 deletions(-) create mode 100644 functions/src/payments/phonepe/invoice/directInvoice.ts create mode 100644 functions/src/payments/phonepe/invoice/getInvoiceUrl.ts create mode 100644 functions/src/payments/phonepe/invoice/index.ts create mode 100644 functions/src/payments/phonepe/invoice/invoiceService.ts create mode 100644 functions/src/payments/phonepe/invoice/processInvoice.ts create mode 100644 functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts create mode 100644 functions/src/payments/phonepe/paymentData.ts create mode 100644 functions/src/utils/emailService.ts diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 90c6243..7862777 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -45,6 +45,7 @@ jobs: sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env + cat functions/.env - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | diff --git a/firebase.json b/firebase.json index d8682a0..dfc4226 100644 --- a/firebase.json +++ b/firebase.json @@ -25,7 +25,7 @@ "port": 5005 }, "firestore": { - "port": 8085 + "port": 8086 }, "storage": { "port": 9199 diff --git a/functions/src/index.ts b/functions/src/index.ts index 1c5b36c..19fa4e0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; -export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments'; +export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; diff --git a/functions/src/payments/phonepe/checkStatus.ts b/functions/src/payments/phonepe/checkStatus.ts index df2fa36..22a9c25 100644 --- a/functions/src/payments/phonepe/checkStatus.ts +++ b/functions/src/payments/phonepe/checkStatus.ts @@ -3,9 +3,13 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request} from "firebase-functions/v2/https"; import { getCorsHandler } from "../../shared/middleware"; import { getAdmin, getLogger } from "../../shared/config"; +import { updatePaymentDataAfterSuccess } from "./paymentData"; +import { InvoiceService } from "./invoice/invoiceService"; + const admin = getAdmin(); const logger = getLogger(); const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); export const checkPhonePePaymentStatus = onRequest({ region: '#{SERVICES_RGN}#' @@ -80,30 +84,144 @@ export const checkPhonePePaymentStatus = onRequest({ .limit(1) .get(); - if (orderQuery.empty) { + if (orderQuery.empty) { logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`); response.status(404).json({ - success: false, - error: 'Payment order not found', - message: `No record found for PhonePe order ID: ${merchantOrderId}` + success: false, + error: 'Payment order not found', + message: `No record found for PhonePe order ID: ${merchantOrderId}` }); return; - } + } - const orderDoc = orderQuery.docs[0]; + const orderDoc = orderQuery.docs[0]; + const orderData = orderDoc.data(); - await orderDoc.ref.update({ + await orderDoc.ref.update({ orderStatus: statusResponse.data.state || 'UNKNOWN', lastChecked: new Date(), statusResponse: statusResponse.data - }); - logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + }); + + if (statusResponse.data.state === 'COMPLETED') { + try { + // Update payment data + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( + merchantOrderId, + statusResponse.data.orderId, + statusResponse.data + ); + + if (paymentUpdateSuccess) { + // Extract membership ID from metaInfo + const membershipId = orderData.metaInfo?.membershipId; + + if (membershipId) { + try { + // Get user data for invoice + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (membershipDoc.exists) { + const membershipData = membershipDoc.data(); + const userId = membershipData?.userId; + + // Get user details + const userDoc = await admin.firestore() + .collection('users') + .doc(userId) + .get(); + + if (userDoc.exists) { + const userData = userDoc.data(); + + // Get gym details + const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; + let gymName = 'Fitlien'; + let gymAddress = ''; + let gstNumber = ''; + + if (gymId) { + const gymDoc = await admin.firestore() + .collection('gyms') + .doc(gymId) + .get(); + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + gstNumber = gymData?.gstNumber || ''; + } + } + + // Generate invoice data + const invoiceData = { + invoiceNumber: `INV-${merchantOrderId.substring(0, 8)}`, + businessName: gymName, + address: gymAddress, + gstNumber: gstNumber, + customerName: userData?.displayName || `${membershipData?.['first-name'] || ''} ${membershipData?.['last-name'] || ''}`.trim(), + phoneNumber: membershipData?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.['email'] || '', + planName: orderData.metaInfo?.planName || 'Membership', + amount: orderData.amount, + transactionId: statusResponse.data.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + + // Update payment record with invoice path + await admin.firestore() + .collection('membership_payments') + .doc(membershipId) + .get() + .then(async (doc) => { + if (doc.exists) { + const paymentsData = doc.data()?.payments || []; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === merchantOrderId || + paymentsData[i].transactionId === statusResponse.data.orderId) { + paymentsData[i].invoicePath = invoicePath; + break; + } + } + + await doc.ref.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + } + }); + + logger.info(`Generated invoice for payment: ${merchantOrderId}, path: ${invoicePath}`); + } + } + } catch (invoiceError) { + logger.error('Error generating invoice:', invoiceError); + // Continue processing - don't fail the response + } + } + } + + logger.info(`Payment data updated for completed payment: ${merchantOrderId}`); + } catch (paymentUpdateError) { + logger.error('Error updating payment data:', paymentUpdateError); + // Continue processing - don't fail the response + } + } - response.json({ - success: true, - state: statusResponse.data.state, - data: statusResponse.data - }); + logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data)); + + response.json({ + success: true, + state: statusResponse.data.state, + data: statusResponse.data + }); } catch (authError: any) { logger.error('Authentication error:', authError); diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts index 258f121..050d5b2 100644 --- a/functions/src/payments/phonepe/index.ts +++ b/functions/src/payments/phonepe/index.ts @@ -1,3 +1,5 @@ export { createPhonePeOrder } from './createPhonepeOrder'; export { checkPhonePePaymentStatus } from './checkStatus'; export { phonePeWebhook } from './webhook'; +export { updatePaymentDataAfterSuccess } from './paymentData'; +export * from './invoice'; diff --git a/functions/src/payments/phonepe/invoice/directInvoice.ts b/functions/src/payments/phonepe/invoice/directInvoice.ts new file mode 100644 index 0000000..93969f5 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/directInvoice.ts @@ -0,0 +1,106 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService, InvoiceData } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const directGenerateInvoice = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + invoiceNumber, + businessName, + address, + gstNumber, + customerName, + phoneNumber, + email, + planName, + amount, + transactionId, + paymentDate, + paymentMethod, + sendEmail, + emailOptions + } = request.body; + + if (!invoiceNumber || !businessName || !customerName || !amount || !transactionId) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const invoiceData: InvoiceData = { + invoiceNumber, + businessName, + address: address || '', + gstNumber, + customerName, + phoneNumber: phoneNumber || '', + email: email || '', + planName: planName || 'Membership', + amount: parseFloat(amount), + transactionId, + paymentDate: paymentDate ? new Date(paymentDate) : new Date(), + paymentMethod: paymentMethod || 'Online' + }; + + // Generate the invoice without updating any payment records + const invoicePath = await invoiceService.generateInvoice(invoiceData); + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + // Send email if requested + let emailSent = false; + if (sendEmail && email) { + emailSent = await invoiceService.sendInvoiceEmail(invoicePath, { + recipientEmail: email, + recipientName: customerName, + ...emailOptions + }); + } + + response.json({ + success: true, + invoicePath, + downloadUrl, + emailSent + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error generating invoice:', error); + response.status(500).json({ + success: false, + error: 'Failed to generate invoice', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts new file mode 100644 index 0000000..038a14c --- /dev/null +++ b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts @@ -0,0 +1,63 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const getInvoiceUrl = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { invoicePath } = request.query; + + if (!invoicePath) { + response.status(400).json({ + success: false, + error: 'Missing invoice path' + }); + return; + } + + // Get a download URL for the invoice + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); + + response.json({ + success: true, + downloadUrl + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error getting invoice URL:', error); + response.status(500).json({ + success: false, + error: 'Failed to get invoice URL', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/index.ts b/functions/src/payments/phonepe/invoice/index.ts new file mode 100644 index 0000000..3d423aa --- /dev/null +++ b/functions/src/payments/phonepe/invoice/index.ts @@ -0,0 +1,12 @@ +import { getInvoiceUrl } from './getInvoiceUrl'; +import { InvoiceService } from './invoiceService'; +import { processInvoice } from './processInvoice'; +import { sendInvoiceEmail } from './sendInvoiceEmail'; + +// Export all invoice-related functions +export { + getInvoiceUrl, + InvoiceService, + processInvoice, + sendInvoiceEmail +}; diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts new file mode 100644 index 0000000..032b259 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -0,0 +1,327 @@ +import { getAdmin, getLogger } from "../../../shared/config"; +import PDFDocument from 'pdfkit'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { format } from 'date-fns'; +import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; + +const admin = getAdmin(); +const logger = getLogger(); + +export interface InvoiceData { + invoiceNumber: string; + businessName: string; + address: string; + gstNumber?: string; + customerName: string; + phoneNumber: string; + email: string; + planName: string; + amount: number; + transactionId: string; + paymentDate: Date; + paymentMethod: string; +} + +export interface EmailOptions { + recipientEmail: string; + recipientName?: string; + subject?: string; + customHtml?: string; + additionalData?: { + gymName?: string; + planName?: string; + amount?: number; + transactionId?: string; + paymentDate?: Date; + paymentMethod?: string; + }; +} + +export class InvoiceService { + async generateInvoice(data: InvoiceData): Promise { + try { + const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); + const doc = new PDFDocument({ margin: 50 }); + + // Create a write stream to the temporary file + const writeStream = fs.createWriteStream(tempFilePath); + doc.pipe(writeStream); + + // Check if GST is applicable + const hasGst = data.gstNumber && data.gstNumber.length > 0; + const baseAmount = hasGst ? data.amount / 1.18 : data.amount; + const sgst = hasGst ? baseAmount * 0.09 : 0; + const cgst = hasGst ? baseAmount * 0.09 : 0; + + // Add business details + doc.fontSize(20).font('Helvetica-Bold').text(data.businessName, 50, 50); + doc.fontSize(12).font('Helvetica').text(data.address, 50, 75, { width: 250 }); + if (hasGst) { + doc.text(`GSTIN: ${data.gstNumber}`, 50, 115); + } + + // Add invoice title and details + doc.fontSize(24).font('Helvetica-Bold').text('RECEIPT', 400, 50, { align: 'right' }); + doc.fontSize(12).font('Helvetica').text(`Receipt #: ${data.invoiceNumber}`, 400, 80, { align: 'right' }); + doc.text(`Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 400, 95, { align: 'right' }); + + // Add customer details + doc.rect(50, 150, 500, 80).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('Receipt To:', 60, 160); + doc.fontSize(12).font('Helvetica').text(data.customerName, 60, 175); + doc.text(`Phone: ${data.phoneNumber}`, 60, 190); + doc.text(`Email: ${data.email}`, 60, 205); + + // Add table header + const tableTop = 260; + doc.rect(50, tableTop, 500, 30).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('No.', 60, tableTop + 10, { width: 30, align: 'center' }); + doc.text('Description', 100, tableTop + 10, { width: 250 }); + doc.text('HSN/SAC', 350, tableTop + 10, { width: 80, align: 'center' }); + doc.text('Amount (INR)', 430, tableTop + 10, { width: 100, align: 'right' }); + + // Add table row + const rowTop = tableTop + 30; + doc.rect(50, rowTop, 500, 30).stroke(); + doc.fontSize(12).font('Helvetica').text('1', 60, rowTop + 10, { width: 30, align: 'center' }); + doc.text(`${data.planName} Subscription`, 100, rowTop + 10, { width: 250 }); + doc.text('999723', 350, rowTop + 10, { width: 80, align: 'center' }); + doc.text(baseAmount.toFixed(2), 430, rowTop + 10, { width: 100, align: 'right' }); + + // Add totals + let currentY = rowTop + 50; + + if (hasGst) { + doc.fontSize(12).font('Helvetica').text('Taxable Amount:', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + + doc.text('SGST (9%):', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${sgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + + doc.text('CGST (9%):', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${cgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + currentY += 20; + } + + doc.moveTo(350, currentY).lineTo(550, currentY).stroke(); + currentY += 10; + + doc.fontSize(12).font('Helvetica-Bold').text('Total Amount:', 350, currentY, { width: 100, align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); + + // Add payment information + currentY += 40; + doc.rect(50, currentY, 500, 80).stroke(); + doc.fontSize(12).font('Helvetica-Bold').text('Payment Information:', 60, currentY + 10); + doc.fontSize(12).font('Helvetica').text(`Transaction ID: ${data.transactionId}`, 60, currentY + 30); + doc.text(`Payment Method: ${data.paymentMethod}`, 60, currentY + 45); + doc.text(`Payment Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 60, currentY + 60); + + // Add footer + currentY += 100; + doc.fontSize(12).font('Helvetica-Oblique').text('Thank you for your business!', 50, currentY, { align: 'center' }); + doc.fontSize(10).font('Helvetica').text('This is a computer-generated receipt and does not require a signature.', 50, currentY + 20, { align: 'center' }); + + // Finalize the PDF + doc.end(); + + // Wait for the file to be written + await new Promise((resolve, reject) => { + writeStream.on('finish', () => resolve()); + writeStream.on('error', reject); + }); + + // Upload to Firebase Storage using admin SDK + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; + const bucket = admin.storage().bucket(); + await bucket.upload(tempFilePath, { + destination: invoicePath, + metadata: { + contentType: 'application/pdf', + }, + }); + + // Clean up the temporary file + fs.unlinkSync(tempFilePath); + + // Return the storage path + return invoicePath; + } catch (error: any) { + logger.error('Error generating invoice:', error); + throw new Error(`Failed to generate invoice: ${error.message}`); + } + } + + async getInvoiceDownloadUrl(invoicePath: string): Promise { + try { + // Using admin SDK to generate a signed URL + const bucket = admin.storage().bucket(); + const file = bucket.file(invoicePath); + + const expirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days + + const [signedUrl] = await file.getSignedUrl({ + action: 'read', + expires: Date.now() + expirationMs, + responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`, + }); + + return signedUrl; + } catch (error: any) { + logger.error('Error getting invoice download URL:', error); + throw new Error(`Failed to get invoice download URL: ${error.message}`); + } + } + + async updateInvoicePath(membershipId: string, paymentId: string, invoicePath: string): Promise { + try { + const membershipPaymentsRef = admin.firestore() + .collection('membership_payments') + .doc(membershipId); + + const docSnapshot = await membershipPaymentsRef.get(); + + if (!docSnapshot.exists) { + logger.error(`No membership payments found for membershipId: ${membershipId}`); + return false; + } + + const data = docSnapshot.data(); + const paymentsData = data?.payments || []; + + // Find the payment by referenceNumber or transactionId + let found = false; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === paymentId || + paymentsData[i].transactionId === paymentId) { + paymentsData[i].invoicePath = invoicePath; + found = true; + break; + } + } + + if (!found) { + logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`); + return false; + } + + await membershipPaymentsRef.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + + logger.info(`Successfully updated invoice path for payment: ${paymentId}`); + return true; + } catch (error: any) { + logger.error('Error updating payment with invoice path:', error); + return false; + } + } + + async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { + try { + // Get the download URL for the invoice + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); + + // Format the date + const formattedDate = emailOptions.additionalData?.paymentDate + ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') + : new Date().toLocaleDateString('en-GB'); + + // Create email HTML content if not provided + const emailHtml = emailOptions.customHtml || ` + + +

Thank you for your payment

+

Dear ${emailOptions.recipientName || 'Valued Customer'},

+

Thank you for your payment. Your membership has been successfully activated.

+

Please find attached your invoice for the payment.

+

Membership Details:

+
    +
  • Gym: ${emailOptions.additionalData?.gymName || 'Fitlien'}
  • +
  • Plan: ${emailOptions.additionalData?.planName || 'Membership'}
  • +
  • Amount: ₹${emailOptions.additionalData?.amount || '0'}
  • +
  • Transaction ID: ${emailOptions.additionalData?.transactionId || 'N/A'}
  • +
  • Date: ${formattedDate}
  • +
  • Payment Method: ${emailOptions.additionalData?.paymentMethod || 'Online'}
  • +
+

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + // Send the email with attachment + await sendEmailWithAttachmentUtil( + emailOptions.recipientEmail, + emailOptions.subject || 'Your Fitlien Membership Invoice', + emailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`); + return true; + } catch (error: any) { + logger.error('Error sending invoice email:', error); + return false; + } + } + + async processInvoice( + membershipId: string, + paymentId: string, + invoiceData: InvoiceData, + emailOptions?: EmailOptions + ): Promise<{ + success: boolean; + invoicePath?: string; + downloadUrl?: string; + emailSent: boolean; + error?: string; + }> { + try { + // Generate the invoice + const invoicePath = await this.generateInvoice(invoiceData); + + // Update the payment record with the invoice path + const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); + + if (!updateSuccess) { + return { + success: false, + invoicePath, + emailSent: false, + error: 'Failed to update payment with invoice path' + }; + } + + // Get a download URL for the invoice + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); + + // Send email if email options are provided + let emailSent = false; + if (emailOptions && emailOptions.recipientEmail) { + emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); + } + + return { + success: true, + invoicePath, + downloadUrl, + emailSent + }; + } catch (error: any) { + logger.error('Error processing invoice:', error); + return { + success: false, + emailSent: false, + error: error.message + }; + } + } +} diff --git a/functions/src/payments/phonepe/invoice/processInvoice.ts b/functions/src/payments/phonepe/invoice/processInvoice.ts new file mode 100644 index 0000000..1164d89 --- /dev/null +++ b/functions/src/payments/phonepe/invoice/processInvoice.ts @@ -0,0 +1,83 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const processInvoice = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + membershipId, + paymentId, + invoiceData, + emailOptions + } = request.body; + + if (!membershipId || !paymentId || !invoiceData) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const result = await invoiceService.processInvoice( + membershipId, + paymentId, + invoiceData, + emailOptions + ); + + if (!result.success) { + response.status(400).json({ + success: false, + error: result.error || 'Failed to process invoice' + }); + return; + } + + response.json({ + success: true, + message: 'Invoice processed successfully', + invoicePath: result.invoicePath, + downloadUrl: result.downloadUrl, + emailSent: result.emailSent + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error processing invoice:', error); + response.status(500).json({ + success: false, + error: 'Failed to process invoice', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts b/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts new file mode 100644 index 0000000..2af677a --- /dev/null +++ b/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts @@ -0,0 +1,91 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { getCorsHandler } from "../../../shared/middleware"; +import { getAdmin, getLogger } from "../../../shared/config"; +import { InvoiceService, EmailOptions } from "./invoiceService"; + +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const invoiceService = new InvoiceService(); + +export const sendInvoiceEmail = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (request: Request, response) => { + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization || ''; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + await admin.auth().verifyIdToken(idToken); + + const { + invoicePath, + recipientEmail, + recipientName, + subject, + customHtml, + gymName, + planName, + amount, + transactionId, + paymentDate, + paymentMethod + } = request.body; + + if (!invoicePath || !recipientEmail) { + response.status(400).json({ + success: false, + error: 'Missing required fields' + }); + return; + } + + const emailOptions: EmailOptions = { + recipientEmail, + recipientName, + subject, + customHtml, + additionalData: { + gymName, + planName, + amount, + transactionId, + paymentDate: paymentDate ? new Date(paymentDate) : undefined, + paymentMethod + } + }; + + const emailSent = await invoiceService.sendInvoiceEmail(invoicePath, emailOptions); + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + response.json({ + success: true, + message: emailSent ? 'Invoice email sent successfully' : 'Failed to send email but URL generated', + downloadUrl + }); + + } catch (authError: any) { + logger.error('Authentication error:', authError); + response.status(401).json({ + success: false, + error: 'Invalid authentication token', + details: authError.message + }); + } + } catch (error: any) { + logger.error('Error sending invoice email:', error); + response.status(500).json({ + success: false, + error: 'Failed to send invoice email', + details: error.message + }); + } + }); +}); diff --git a/functions/src/payments/phonepe/paymentData.ts b/functions/src/payments/phonepe/paymentData.ts new file mode 100644 index 0000000..b6eca48 --- /dev/null +++ b/functions/src/payments/phonepe/paymentData.ts @@ -0,0 +1,130 @@ +import { getAdmin, getLogger } from "../../shared/config"; + +const admin = getAdmin(); +const logger = getLogger(); + +// Define an interface for the payment data to avoid type errors +interface PaymentData { + id: string; + date: string; + dateTimestamp: FirebaseFirestore.Timestamp; + amount: any; + paymentMethod: string; + referenceNumber: string; + discount: any; + transactionId: string; + createdAt: Date; + invoicePath?: string; // Make this optional +} + +export async function updatePaymentDataAfterSuccess( + merchantOrderId: string, + orderId: string, + paymentDetails: any, + invoicePath?: string +): Promise { + try { + // Get the payment order from Firestore + const orderQuery = await admin.firestore() + .collection('payment_orders') + .where('merchantOrderId', '==', merchantOrderId) + .limit(1) + .get(); + + if (orderQuery.empty) { + logger.error(`No payment order found with merchantOrderId: ${merchantOrderId}`); + return false; + } + + const orderDoc = orderQuery.docs[0]; + const orderData = orderDoc.data(); + + // Extract membership ID from metaInfo + const membershipId = orderData.metaInfo?.membershipId; + if (!membershipId) { + logger.error(`No membershipId found in metaInfo for order: ${merchantOrderId}`); + return false; + } + + const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); // DD-MM-YYYY format + const dateTimestamp = admin.firestore.Timestamp.now(); + + // Create payment data object with proper typing + const paymentData: PaymentData = { + id: admin.firestore().collection('_').doc().id, // Generate a UUID + date: isoDate, + dateTimestamp: dateTimestamp, + amount: orderData.amount, + paymentMethod: 'Online', + referenceNumber: merchantOrderId, + discount: orderData.metaInfo?.discount || null, + transactionId: orderId, + createdAt: new Date() + }; + + // Add invoice path if provided + if (invoicePath) { + paymentData.invoicePath = invoicePath; + } + + // Get reference to membership payments document + const membershipPaymentsRef = admin.firestore() + .collection('membership_payments') + .doc(membershipId); + + const docSnapshot = await membershipPaymentsRef.get(); + + // Update or create the membership payments document + if (docSnapshot.exists) { + await membershipPaymentsRef.update({ + 'payments': admin.firestore.FieldValue.arrayUnion(paymentData), + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + 'updatedBy': orderData.userId, + }); + } else { + await membershipPaymentsRef.set({ + 'membershipId': membershipId, + 'payments': [paymentData], + 'createdAt': admin.firestore.FieldValue.serverTimestamp(), + 'createdBy': orderData.userId, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + 'updatedBy': orderData.userId, + }); + } + + // Update membership status + await updateMembershipStatus(membershipId, orderData.userId); + + logger.info(`Successfully updated payment data for membership: ${membershipId}`); + return true; + } catch (error: any) { + logger.error('Error updating payment data:', error); + return false; + } +} + +async function updateMembershipStatus(membershipId: string, userId: string): Promise { + try { + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (!membershipDoc.exists) { + throw new Error(`Membership not found for id: ${membershipId}`); + } + + await admin.firestore() + .collection('memberships') + .doc(membershipId) + .update({ + 'status': 'ACTIVE', // Assuming this matches your InvitationStatus.active + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + + logger.info(`Successfully updated membership status for: ${membershipId}`); + } catch (error: any) { + logger.error('Error updating membership status:', error); + throw error; + } +} diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9f522f9..9a5f497 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -2,15 +2,20 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; import { getAdmin, getLogger } from "../../shared/config"; import crypto from "crypto"; +import { updatePaymentDataAfterSuccess } from "./paymentData"; +import { InvoiceService } from "./invoice/invoiceService"; +import * as path from 'path'; +import { sendEmailWithAttachmentUtil } from "../../utils/emailService"; +import { format } from 'date-fns'; const admin = getAdmin(); const logger = getLogger(); +const invoiceService = new InvoiceService(); export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { - logger.info('Received webhook request', { headers: request.headers, body: request.body, @@ -20,7 +25,7 @@ export const phonePeWebhook = onRequest({ const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; - + if (!authHeader || !username || !password) { logger.error('Missing authorization header or webhook credentials'); response.status(401).json({ error: 'Unauthorized' }); @@ -34,7 +39,7 @@ export const phonePeWebhook = onRequest({ .digest('hex'); const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); - + if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { logger.error('Invalid webhook authorization'); response.status(401).json({ error: 'Invalid authorization' }); @@ -42,32 +47,34 @@ export const phonePeWebhook = onRequest({ } const { event, payload } = request.body; - + if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { logger.error('Invalid webhook payload', request.body); response.status(400).json({ error: 'Invalid payload' }); return; } - - logger.info(`Received PhonePe webhook: ${event}`, { + + logger.info(`Received PhonePe webhook: ${event}`, { merchantOrderId: payload.merchantOrderId, orderId: payload.orderId, state: payload.state }); - + const orderQuery = await admin.firestore() .collection('payment_orders') .where('orderId', '==', payload.orderId) .limit(1) .get(); - + + let orderDoc; + if (orderQuery.empty) { const merchantOrderQuery = await admin.firestore() .collection('payment_orders') .where('merchantOrderId', '==', payload.merchantOrderId) .limit(1) .get(); - + if (merchantOrderQuery.empty) { logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); response.status(404).json({ @@ -76,30 +83,325 @@ export const phonePeWebhook = onRequest({ }); return; } - - const orderDoc = merchantOrderQuery.docs[0]; + + orderDoc = merchantOrderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); - + logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); } else { - const orderDoc = orderQuery.docs[0]; + orderDoc = orderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); - + logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } - + + if (payload.state === 'COMPLETED') { + try { + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( + payload.merchantOrderId, + payload.orderId, + payload + ); + + if (paymentUpdateSuccess) { + const orderData = orderDoc.data(); + + const membershipId = orderData.metaInfo?.membershipId; + + if (membershipId) { + try { + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (membershipDoc.exists) { + const membershipData = membershipDoc.data(); + const userId = membershipData?.userId; + + const userDoc = await admin.firestore() + .collection('users') + .doc(userId) + .get(); + + if (userDoc.exists) { + const userData = userDoc.data(); + + const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; + let gymName = 'Fitlien'; + let gymAddress = ''; + let subscriptionName = ''; + let gymOwnerEmail = ''; + let gymPhoneNumber = ''; + let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; + let trainerId = orderData.metaInfo?.trainerId; + let trainerData = null; + + const discountPercentage = orderData.metaInfo?.discount || 0; + const hasDiscount = discountPercentage > 0; + const isFreeplan = discountPercentage === 100; + const originalAmount = hasDiscount ? + orderData.amount / (1 - discountPercentage / 100) : + orderData.amount; + const discountText = isFreeplan ? + " (Free Plan)" : + hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` : + ''; + const amountSaved = hasDiscount ? + originalAmount - orderData.amount : + 0; + + if (gymId) { + const gymDoc = await admin.firestore() + .collection('gyms') + .doc(gymId) + .get(); + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + subscriptionName = gymData?.subscriptions?.name || ''; + gymPhoneNumber = gymData?.phoneNumber || ''; + + if (gymData?.userId) { + const gymOwnerDoc = await admin.firestore() + .collection('users') + .doc(gymData.userId) + .get(); + + if (gymOwnerDoc.exists) { + const gymOwnerData = gymOwnerDoc.data(); + gymOwnerEmail = gymOwnerData?.email || ''; + } + } + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerId) { + try { + const trainerDoc = await admin.firestore() + .collection('trainer_profiles') + .doc(trainerId) + .get(); + + if (trainerDoc.exists) { + trainerData = trainerDoc.data(); + } + } catch (trainerError) { + logger.error('Error fetching trainer data:', trainerError); + } + } + + const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; + const invoiceData = { + invoiceNumber, + businessName: gymName, + address: gymAddress, + gstNumber: userData?.gstNumber, + customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), + phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.fields?.['email'] || '', + planName: orderData.metaInfo?.planName || subscriptionName || paymentType, + amount: orderData.amount, + transactionId: payload.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + + await admin.firestore() + .collection('membership_payments') + .doc(membershipId) + .get() + .then(async (doc) => { + if (doc.exists) { + const paymentsData = doc.data()?.payments || []; + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === payload.merchantOrderId || + paymentsData[i].transactionId === payload.orderId) { + paymentsData[i].invoicePath = invoicePath; + break; + } + } + + await doc.ref.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + } + }); + + logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`); + + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + + const formattedDate = format(new Date(), 'dd/MM/yyyy'); + + if (membershipData?.fields?.['email']) { + try { + const emailSubject = isFreeplan + ? `Free Plan Assigned - ${gymName}` + : `New Membership - ${gymName}`; + + const customerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}

+

Dear ${invoiceData.customerName},

+

${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}

+

Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.

+

Membership Details:

+
    +
  • Gym: ${gymName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Your Personal Trainer'}
  • ` : ''} +
  • Plan: ${invoiceData.planName}
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • You Save: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • + ${isFreeplan ? '
  • Payment Method: Online}
  • ' : ''} +
+

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + membershipData?.fields?.['email'], + emailSubject, + customerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to ${membershipData?.fields?.['email']} for payment: ${payload.merchantOrderId}`); + } catch (emailError) { + logger.error('Error sending customer invoice email:', emailError); + } + } + + if (gymOwnerEmail) { + try { + const ownerEmailSubject = isFreeplan + ? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}` + : `New Membership${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`; + + const gymOwnerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}

+

Dear Gym Owner,

+

${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.

+

Customer Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: ${invoiceData.planName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Personal Trainer'}
  • ` : ''} + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • Amount Saved by Customer: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + gymOwnerEmail, + ownerEmailSubject, + gymOwnerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); + } catch (ownerEmailError) { + logger.error('Error sending gym owner invoice email:', ownerEmailError); + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerData && trainerData.email) { + try { + const trainerEmailHtml = ` + + +

New Personal Training Client

+

Dear ${trainerData.fullName || 'Trainer'},

+

A new client has signed up for personal training with you at ${gymName}.

+

Client Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: Personal Training Membership
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + trainerData.email, + `New Personal Training Client - ${gymName}`, + trainerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to trainer (${trainerData.email}) for payment: ${payload.merchantOrderId}`); + } catch (trainerEmailError) { + logger.error('Error sending trainer invoice email:', trainerEmailError); + } + } + } + } + } catch (invoiceError) { + logger.error('Error generating invoice:', invoiceError); + } + } + } + + logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`); + } catch (paymentUpdateError) { + logger.error('Error updating payment data:', paymentUpdateError); + } + } + response.status(200).json({ success: true }); - + } catch (error: any) { logger.error('PhonePe webhook processing error:', error); response.status(500).json({ diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts new file mode 100644 index 0000000..b7f70a8 --- /dev/null +++ b/functions/src/utils/emailService.ts @@ -0,0 +1,70 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as https from 'https'; +import { getLogger } from "../shared/config"; +import formData from 'form-data'; +import Mailgun from 'mailgun.js'; +const { convert } = require('html-to-text'); + +const mailgun = new Mailgun(formData); +const logger = getLogger(); + +export async function sendEmailWithAttachmentUtil( + toAddress: string, + subject: string, + message: string, + fileUrl: string, + fileName?: string +): Promise { + try { + const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); + await new Promise((resolve, reject) => { + const file = fs.createWriteStream(tempFilePath); + https.get(fileUrl, (res) => { + res.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }).on('error', (err) => { + fs.unlink(tempFilePath, () => {}); + reject(err); + }); + }); + + try { + const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); + const options = { + wordwrap: 130, + }; + const textMessage = convert(message, options); + const fileBuffer = fs.readFileSync(tempFilePath); + const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); + + const data = { + from: process.env.MAILGUN_FROM_ADDRESS, + to: toAddress, + subject: subject, + text: textMessage, + html: message, + attachment: { + data: fileBuffer, + filename: attachmentFilename, + contentType: 'application/pdf', + } + }; + + const result = await client.messages.create(process.env.MAILGUN_SERVER!, data); + fs.unlinkSync(tempFilePath); + logger.info('Email with attachment from URL sent successfully'); + return { success: true, result }; + } catch (e) { + logger.error(`Error while sending E-mail. Error: ${e}`); + throw e; + } + } catch (error) { + logger.error('Error sending email with attachment from URL:', error); + throw error; + } +} diff --git a/package-lock.json b/package-lock.json index 6c1f8fe..b0bc379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,20 @@ "": { "dependencies": { "@types/busboy": "^1.5.4", - "busboy": "^1.6.0" - }, - "devDependencies": { - "@types/long": "^5.0.0" + "@types/nodemailer": "^6.4.17", + "@types/pdfkit": "^0.13.9", + "busboy": "^1.6.0", + "date-fns": "^4.1.0", + "nodemailer": "^7.0.3", + "pdfkit": "^0.17.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": { + "tslib": "^2.8.0" } }, "node_modules/@types/busboy": { @@ -20,16 +30,6 @@ "@types/node": "*" } }, - "node_modules/@types/long": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", - "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", - "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "long": "*" - } - }, "node_modules/@types/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", @@ -38,6 +38,49 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfkit": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz", + "integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -49,11 +92,110 @@ "node": ">=10.16.0" } }, - "node_modules/long": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", - "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", - "dev": true + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==" + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/pdfkit": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz", + "integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==" }, "node_modules/streamsearch": { "version": "1.1.0", @@ -63,10 +205,38 @@ "node": ">=10.0.0" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } } } } diff --git a/package.json b/package.json index 474c153..073e12e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,11 @@ { "dependencies": { "@types/busboy": "^1.5.4", - "busboy": "^1.6.0" + "@types/nodemailer": "^6.4.17", + "@types/pdfkit": "^0.13.9", + "busboy": "^1.6.0", + "date-fns": "^4.1.0", + "nodemailer": "^7.0.3", + "pdfkit": "^0.17.1" } } From 2d55e1f4615726887ab90a2c555c20ee64e9e131 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 19 May 2025 13:18:52 +0530 Subject: [PATCH 46/81] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9a5f497..05e4ee9 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -142,7 +142,6 @@ export const phonePeWebhook = onRequest({ let gymAddress = ''; let subscriptionName = ''; let gymOwnerEmail = ''; - let gymPhoneNumber = ''; let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; let trainerId = orderData.metaInfo?.trainerId; let trainerData = null; @@ -172,7 +171,6 @@ export const phonePeWebhook = onRequest({ gymName = gymData?.name || 'Fitlien'; gymAddress = gymData?.address || ''; subscriptionName = gymData?.subscriptions?.name || ''; - gymPhoneNumber = gymData?.phoneNumber || ''; if (gymData?.userId) { const gymOwnerDoc = await admin.firestore() From d335fb38e94c7a8d4dbe78a798b4954165491235 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Mon, 19 May 2025 07:52:03 +0000 Subject: [PATCH 47/81] phonepe (#28) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/28 --- functions/src/payments/phonepe/webhook.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9a5f497..05e4ee9 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -142,7 +142,6 @@ export const phonePeWebhook = onRequest({ let gymAddress = ''; let subscriptionName = ''; let gymOwnerEmail = ''; - let gymPhoneNumber = ''; let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; let trainerId = orderData.metaInfo?.trainerId; let trainerData = null; @@ -172,7 +171,6 @@ export const phonePeWebhook = onRequest({ gymName = gymData?.name || 'Fitlien'; gymAddress = gymData?.address || ''; subscriptionName = gymData?.subscriptions?.name || ''; - gymPhoneNumber = gymData?.phoneNumber || ''; if (gymData?.userId) { const gymOwnerDoc = await admin.firestore() From 2d6b14663b15be002b000bc9b3c0c59b90a90724 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 19 May 2025 14:28:51 +0530 Subject: [PATCH 48/81] changed pdf to use encoding supported package --- functions/package-lock.json | 286 ++++++++++++++++ functions/package.json | 2 + .../phonepe/invoice/invoiceService.ts | 305 ++++++++++++------ 3 files changed, 498 insertions(+), 95 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index b53c64d..9feb2ad 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -22,12 +22,14 @@ "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", + "pdfmake": "^0.2.20", "twilio": "^5.4.0" }, "devDependencies": { "@types/long": "^5.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", + "@types/pdfmake": "^0.2.11", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, @@ -1260,6 +1262,52 @@ "tslib": "^2.1.0" } }, + "node_modules/@foliojs-fork/fontkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", + "dependencies": { + "@foliojs-fork/restructure": "^2.0.2", + "brotli": "^1.2.0", + "clone": "^1.0.4", + "deep-equal": "^1.0.0", + "dfa": "^1.2.0", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.2.2", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", + "dependencies": { + "base64-js": "1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak/node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "node_modules/@foliojs-fork/pdfkit": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", + "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", + "dependencies": { + "@foliojs-fork/fontkit": "^1.9.2", + "@foliojs-fork/linebreak": "^1.1.1", + "crypto-js": "^4.2.0", + "jpeg-exif": "^1.1.4", + "png-js": "^1.0.0" + } + }, + "node_modules/@foliojs-fork/restructure": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", + "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==" + }, "node_modules/@google-cloud/firestore": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", @@ -2787,6 +2835,25 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/pdfkit": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz", + "integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfmake": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.2.11.tgz", + "integrity": "sha512-gglgMQhnG6C2kco13DJlvokqTxL+XKxHwCejElH8fSCNF9ZCkRK6Mzo011jQ0zuug+YlIgn6BpcpZrARyWdW3Q==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/pdfkit": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", @@ -3261,6 +3328,14 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -3483,6 +3558,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3625,6 +3708,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -3653,6 +3741,25 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3677,6 +3784,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3712,6 +3835,11 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -4351,6 +4479,14 @@ "resolved": "", "link": true }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -4950,6 +5086,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5772,6 +5923,11 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6366,6 +6522,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6456,6 +6635,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6548,6 +6732,31 @@ "@napi-rs/canvas": "^0.1.67" } }, + "node_modules/pdfmake": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz", + "integrity": "sha512-bGbxbGFP5p8PWNT3Phsu1ZcRLnRfF6jmnuKTkgmt6i5PZzSdX6JaB+NeTz9q+aocfW8SE9GUjL3o/5GroBqGcQ==", + "dependencies": { + "@foliojs-fork/linebreak": "^1.1.2", + "@foliojs-fork/pdfkit": "^0.15.3", + "iconv-lite": "^0.6.3", + "xmldoc": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pdfmake/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", @@ -6599,6 +6808,11 @@ "node": ">=8" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6789,6 +7003,25 @@ "node": ">= 6" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7023,6 +7256,20 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7424,6 +7671,11 @@ "node": ">=8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7582,6 +7834,24 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7842,6 +8112,22 @@ "node": ">=6.0" } }, + "node_modules/xmldoc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.1.tgz", + "integrity": "sha512-sOOqgsjl3PU6iBw+fBUGAkTCE+JFK+sBaOL3pnZgzqk2/yvOD7RlFmZtDRJAEBzdpOYxSXyOQH4mjubdfs3MSg==", + "dependencies": { + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/xmldoc/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/functions/package.json b/functions/package.json index f83c0fd..bbfdd3f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -29,12 +29,14 @@ "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", + "pdfmake": "^0.2.20", "twilio": "^5.4.0" }, "devDependencies": { "@types/long": "^5.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", + "@types/pdfmake": "^0.2.11", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 032b259..8f2db0b 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -1,10 +1,13 @@ import { getAdmin, getLogger } from "../../../shared/config"; -import PDFDocument from 'pdfkit'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; +import * as pdfMake from 'pdfmake/build/pdfmake'; +import * as pdfFonts from 'pdfmake/build/vfs_fonts'; + +(pdfMake as any).vfs = pdfFonts.vfs; const admin = getAdmin(); const logger = getLogger(); @@ -43,99 +46,223 @@ export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); - const doc = new PDFDocument({ margin: 50 }); - // Create a write stream to the temporary file - const writeStream = fs.createWriteStream(tempFilePath); - doc.pipe(writeStream); - - // Check if GST is applicable const hasGst = data.gstNumber && data.gstNumber.length > 0; const baseAmount = hasGst ? data.amount / 1.18 : data.amount; const sgst = hasGst ? baseAmount * 0.09 : 0; const cgst = hasGst ? baseAmount * 0.09 : 0; - // Add business details - doc.fontSize(20).font('Helvetica-Bold').text(data.businessName, 50, 50); - doc.fontSize(12).font('Helvetica').text(data.address, 50, 75, { width: 250 }); - if (hasGst) { - doc.text(`GSTIN: ${data.gstNumber}`, 50, 115); - } + const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); - // Add invoice title and details - doc.fontSize(24).font('Helvetica-Bold').text('RECEIPT', 400, 50, { align: 'right' }); - doc.fontSize(12).font('Helvetica').text(`Receipt #: ${data.invoiceNumber}`, 400, 80, { align: 'right' }); - doc.text(`Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 400, 95, { align: 'right' }); + const docDefinition: any = { + content: [ + { + columns: [ + [ + { text: data.businessName, style: 'businessName' }, + { text: data.address, style: 'businessAddress' }, + hasGst ? { text: `GSTIN: ${data.gstNumber}`, style: 'businessDetails' } : {} + ], + [ + { text: 'RECEIPT', style: 'invoiceTitle', alignment: 'right' }, + { text: `Receipt #: ${data.invoiceNumber}`, style: 'invoiceDetails', alignment: 'right' }, + { text: `Date: ${formattedDate}`, style: 'invoiceDetails', alignment: 'right' } + ] + ] + }, + { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 0.5 }] }, + { text: '', margin: [0, 10] }, + + { + style: 'customerBox', + table: { + widths: ['*'], + body: [ + [ + { + stack: [ + { text: 'Receipt To:', style: 'customerTitle' }, + { text: data.customerName, style: 'customerDetails' }, + { text: `Phone: ${data.phoneNumber}`, style: 'customerDetails' }, + { text: `Email: ${data.email}`, style: 'customerDetails' } + ], + margin: [10, 10] + } + ] + ] + }, + layout: 'lightHorizontalLines' + }, + { text: '', margin: [0, 10] }, + + { + table: { + headerRows: 1, + widths: [30, '*', 80, 100], + body: [ + [ + { text: 'No.', style: 'tableHeader', alignment: 'center' }, + { text: 'Description', style: 'tableHeader' }, + { text: 'HSN/SAC', style: 'tableHeader', alignment: 'center' }, + { text: 'Amount (INR)', style: 'tableHeader', alignment: 'right' } + ], + [ + { text: '1', alignment: 'center' }, + { text: `${data.planName} Subscription` }, + { text: '999723', alignment: 'center' }, + { text: baseAmount.toFixed(2), alignment: 'right' } + ] + ] + } + }, + { text: '', margin: [0, 10] }, + + { + columns: [ + { width: '*', text: '' }, + { + width: 'auto', + table: { + widths: [100, 100], + body: hasGst ? [ + [ + { text: 'Taxable Amount:', alignment: 'right' }, + { text: `${baseAmount.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'SGST (9%):', alignment: 'right' }, + { text: `${sgst.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'CGST (9%):', alignment: 'right' }, + { text: `${cgst.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, + { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } + ] + ] : [ + [ + { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, + { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } + ] + ] + }, + layout: { + hLineWidth: function(i: number, node: any) { + return (i === node.table.body.length - 1) ? 0.5 : 0; + }, + vLineWidth: function() { return 0; } + } + } + ] + }, + { text: '', margin: [0, 20] }, + + { + style: 'paymentBox', + table: { + widths: ['*'], + body: [ + [ + { + stack: [ + { text: 'Payment Information:', style: 'paymentTitle' }, + { text: `Transaction ID: ${data.transactionId}`, style: 'paymentDetails' }, + { text: `Payment Method: ${data.paymentMethod}`, style: 'paymentDetails' }, + { text: `Payment Date: ${formattedDate}`, style: 'paymentDetails' } + ], + margin: [10, 10] + } + ] + ] + }, + layout: 'lightHorizontalLines' + }, + { text: '', margin: [0, 20] }, + + { text: 'Thank you for your business!', style: 'footer', alignment: 'center' }, + { text: 'This is a computer-generated receipt and does not require a signature.', style: 'disclaimer', alignment: 'center' } + ], + styles: { + businessName: { + fontSize: 20, + bold: true, + margin: [0, 0, 0, 5] + }, + businessAddress: { + fontSize: 12, + margin: [0, 0, 0, 5] + }, + businessDetails: { + fontSize: 12 + }, + invoiceTitle: { + fontSize: 24, + bold: true + }, + invoiceDetails: { + fontSize: 12, + margin: [0, 5, 0, 0] + }, + customerBox: { + margin: [0, 10, 0, 10] + }, + customerTitle: { + fontSize: 12, + bold: true, + margin: [0, 0, 0, 5] + }, + customerDetails: { + fontSize: 12, + margin: [0, 2, 0, 0] + }, + tableHeader: { + fontSize: 12, + bold: true, + margin: [0, 5, 0, 5] + }, + totalAmount: { + fontSize: 12, + bold: true + }, + paymentBox: { + margin: [0, 10, 0, 10] + }, + paymentTitle: { + fontSize: 12, + bold: true, + margin: [0, 0, 0, 5] + }, + paymentDetails: { + fontSize: 12, + margin: [0, 2, 0, 0] + }, + footer: { + fontSize: 12, + italics: true, + margin: [0, 0, 0, 5] + }, + disclaimer: { + fontSize: 10 + } + }, + defaultStyle: { + font: 'Helvetica' + } + }; - // Add customer details - doc.rect(50, 150, 500, 80).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('Receipt To:', 60, 160); - doc.fontSize(12).font('Helvetica').text(data.customerName, 60, 175); - doc.text(`Phone: ${data.phoneNumber}`, 60, 190); - doc.text(`Email: ${data.email}`, 60, 205); + const pdfDoc = pdfMake.createPdf(docDefinition); - // Add table header - const tableTop = 260; - doc.rect(50, tableTop, 500, 30).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('No.', 60, tableTop + 10, { width: 30, align: 'center' }); - doc.text('Description', 100, tableTop + 10, { width: 250 }); - doc.text('HSN/SAC', 350, tableTop + 10, { width: 80, align: 'center' }); - doc.text('Amount (INR)', 430, tableTop + 10, { width: 100, align: 'right' }); - - // Add table row - const rowTop = tableTop + 30; - doc.rect(50, rowTop, 500, 30).stroke(); - doc.fontSize(12).font('Helvetica').text('1', 60, rowTop + 10, { width: 30, align: 'center' }); - doc.text(`${data.planName} Subscription`, 100, rowTop + 10, { width: 250 }); - doc.text('999723', 350, rowTop + 10, { width: 80, align: 'center' }); - doc.text(baseAmount.toFixed(2), 430, rowTop + 10, { width: 100, align: 'right' }); - - // Add totals - let currentY = rowTop + 50; - - if (hasGst) { - doc.fontSize(12).font('Helvetica').text('Taxable Amount:', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${baseAmount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - - doc.text('SGST (9%):', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${sgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - - doc.text('CGST (9%):', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${cgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - } - - doc.moveTo(350, currentY).lineTo(550, currentY).stroke(); - currentY += 10; - - doc.fontSize(12).font('Helvetica-Bold').text('Total Amount:', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${data.amount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - - // Add payment information - currentY += 40; - doc.rect(50, currentY, 500, 80).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('Payment Information:', 60, currentY + 10); - doc.fontSize(12).font('Helvetica').text(`Transaction ID: ${data.transactionId}`, 60, currentY + 30); - doc.text(`Payment Method: ${data.paymentMethod}`, 60, currentY + 45); - doc.text(`Payment Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 60, currentY + 60); - - // Add footer - currentY += 100; - doc.fontSize(12).font('Helvetica-Oblique').text('Thank you for your business!', 50, currentY, { align: 'center' }); - doc.fontSize(10).font('Helvetica').text('This is a computer-generated receipt and does not require a signature.', 50, currentY + 20, { align: 'center' }); - - // Finalize the PDF - doc.end(); - - // Wait for the file to be written await new Promise((resolve, reject) => { - writeStream.on('finish', () => resolve()); - writeStream.on('error', reject); + pdfDoc.getBuffer((buffer) => { + fs.writeFile(tempFilePath, buffer, (err) => { + if (err) reject(err); + else resolve(); + }); + }); }); - // Upload to Firebase Storage using admin SDK const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { @@ -145,10 +272,8 @@ export class InvoiceService { }, }); - // Clean up the temporary file fs.unlinkSync(tempFilePath); - // Return the storage path return invoicePath; } catch (error: any) { logger.error('Error generating invoice:', error); @@ -158,11 +283,10 @@ export class InvoiceService { async getInvoiceDownloadUrl(invoicePath: string): Promise { try { - // Using admin SDK to generate a signed URL const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); - const expirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days + const expirationMs = 7 * 24 * 60 * 60 * 1000; const [signedUrl] = await file.getSignedUrl({ action: 'read', @@ -193,7 +317,6 @@ export class InvoiceService { const data = docSnapshot.data(); const paymentsData = data?.payments || []; - // Find the payment by referenceNumber or transactionId let found = false; for (let i = 0; i < paymentsData.length; i++) { if (paymentsData[i].referenceNumber === paymentId || @@ -224,15 +347,12 @@ export class InvoiceService { async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { try { - // Get the download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - // Format the date const formattedDate = emailOptions.additionalData?.paymentDate ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') : new Date().toLocaleDateString('en-GB'); - // Create email HTML content if not provided const emailHtml = emailOptions.customHtml || ` @@ -255,7 +375,6 @@ export class InvoiceService { `; - // Send the email with attachment await sendEmailWithAttachmentUtil( emailOptions.recipientEmail, emailOptions.subject || 'Your Fitlien Membership Invoice', @@ -285,10 +404,8 @@ export class InvoiceService { error?: string; }> { try { - // Generate the invoice const invoicePath = await this.generateInvoice(invoiceData); - // Update the payment record with the invoice path const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); if (!updateSuccess) { @@ -300,10 +417,8 @@ export class InvoiceService { }; } - // Get a download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - // Send email if email options are provided let emailSent = false; if (emailOptions && emailOptions.recipientEmail) { emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); From 91e2af5890c8ea87640c0e6b757bb16017fcc798 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Mon, 19 May 2025 08:59:37 +0000 Subject: [PATCH 49/81] phonepe (#29) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/29 --- functions/package-lock.json | 286 ++++++++++++++++ functions/package.json | 2 + .../phonepe/invoice/invoiceService.ts | 305 ++++++++++++------ 3 files changed, 498 insertions(+), 95 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index b53c64d..9feb2ad 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -22,12 +22,14 @@ "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", + "pdfmake": "^0.2.20", "twilio": "^5.4.0" }, "devDependencies": { "@types/long": "^5.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", + "@types/pdfmake": "^0.2.11", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, @@ -1260,6 +1262,52 @@ "tslib": "^2.1.0" } }, + "node_modules/@foliojs-fork/fontkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", + "dependencies": { + "@foliojs-fork/restructure": "^2.0.2", + "brotli": "^1.2.0", + "clone": "^1.0.4", + "deep-equal": "^1.0.0", + "dfa": "^1.2.0", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.2.2", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", + "dependencies": { + "base64-js": "1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak/node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "node_modules/@foliojs-fork/pdfkit": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", + "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", + "dependencies": { + "@foliojs-fork/fontkit": "^1.9.2", + "@foliojs-fork/linebreak": "^1.1.1", + "crypto-js": "^4.2.0", + "jpeg-exif": "^1.1.4", + "png-js": "^1.0.0" + } + }, + "node_modules/@foliojs-fork/restructure": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", + "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==" + }, "node_modules/@google-cloud/firestore": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", @@ -2787,6 +2835,25 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/pdfkit": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz", + "integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfmake": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.2.11.tgz", + "integrity": "sha512-gglgMQhnG6C2kco13DJlvokqTxL+XKxHwCejElH8fSCNF9ZCkRK6Mzo011jQ0zuug+YlIgn6BpcpZrARyWdW3Q==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/pdfkit": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", @@ -3261,6 +3328,14 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -3483,6 +3558,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3625,6 +3708,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -3653,6 +3741,25 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3677,6 +3784,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3712,6 +3835,11 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -4351,6 +4479,14 @@ "resolved": "", "link": true }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -4950,6 +5086,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5772,6 +5923,11 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6366,6 +6522,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6456,6 +6635,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6548,6 +6732,31 @@ "@napi-rs/canvas": "^0.1.67" } }, + "node_modules/pdfmake": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz", + "integrity": "sha512-bGbxbGFP5p8PWNT3Phsu1ZcRLnRfF6jmnuKTkgmt6i5PZzSdX6JaB+NeTz9q+aocfW8SE9GUjL3o/5GroBqGcQ==", + "dependencies": { + "@foliojs-fork/linebreak": "^1.1.2", + "@foliojs-fork/pdfkit": "^0.15.3", + "iconv-lite": "^0.6.3", + "xmldoc": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pdfmake/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", @@ -6599,6 +6808,11 @@ "node": ">=8" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6789,6 +7003,25 @@ "node": ">= 6" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7023,6 +7256,20 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7424,6 +7671,11 @@ "node": ">=8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7582,6 +7834,24 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7842,6 +8112,22 @@ "node": ">=6.0" } }, + "node_modules/xmldoc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.1.tgz", + "integrity": "sha512-sOOqgsjl3PU6iBw+fBUGAkTCE+JFK+sBaOL3pnZgzqk2/yvOD7RlFmZtDRJAEBzdpOYxSXyOQH4mjubdfs3MSg==", + "dependencies": { + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/xmldoc/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/functions/package.json b/functions/package.json index f83c0fd..bbfdd3f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -29,12 +29,14 @@ "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", + "pdfmake": "^0.2.20", "twilio": "^5.4.0" }, "devDependencies": { "@types/long": "^5.0.0", "@types/mime-types": "^2.1.4", "@types/node": "^22.13.14", + "@types/pdfmake": "^0.2.11", "firebase-functions-test": "^3.1.0", "typescript": "^5.8.2" }, diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 032b259..8f2db0b 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -1,10 +1,13 @@ import { getAdmin, getLogger } from "../../../shared/config"; -import PDFDocument from 'pdfkit'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; +import * as pdfMake from 'pdfmake/build/pdfmake'; +import * as pdfFonts from 'pdfmake/build/vfs_fonts'; + +(pdfMake as any).vfs = pdfFonts.vfs; const admin = getAdmin(); const logger = getLogger(); @@ -43,99 +46,223 @@ export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); - const doc = new PDFDocument({ margin: 50 }); - // Create a write stream to the temporary file - const writeStream = fs.createWriteStream(tempFilePath); - doc.pipe(writeStream); - - // Check if GST is applicable const hasGst = data.gstNumber && data.gstNumber.length > 0; const baseAmount = hasGst ? data.amount / 1.18 : data.amount; const sgst = hasGst ? baseAmount * 0.09 : 0; const cgst = hasGst ? baseAmount * 0.09 : 0; - // Add business details - doc.fontSize(20).font('Helvetica-Bold').text(data.businessName, 50, 50); - doc.fontSize(12).font('Helvetica').text(data.address, 50, 75, { width: 250 }); - if (hasGst) { - doc.text(`GSTIN: ${data.gstNumber}`, 50, 115); - } + const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); - // Add invoice title and details - doc.fontSize(24).font('Helvetica-Bold').text('RECEIPT', 400, 50, { align: 'right' }); - doc.fontSize(12).font('Helvetica').text(`Receipt #: ${data.invoiceNumber}`, 400, 80, { align: 'right' }); - doc.text(`Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 400, 95, { align: 'right' }); + const docDefinition: any = { + content: [ + { + columns: [ + [ + { text: data.businessName, style: 'businessName' }, + { text: data.address, style: 'businessAddress' }, + hasGst ? { text: `GSTIN: ${data.gstNumber}`, style: 'businessDetails' } : {} + ], + [ + { text: 'RECEIPT', style: 'invoiceTitle', alignment: 'right' }, + { text: `Receipt #: ${data.invoiceNumber}`, style: 'invoiceDetails', alignment: 'right' }, + { text: `Date: ${formattedDate}`, style: 'invoiceDetails', alignment: 'right' } + ] + ] + }, + { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 0.5 }] }, + { text: '', margin: [0, 10] }, + + { + style: 'customerBox', + table: { + widths: ['*'], + body: [ + [ + { + stack: [ + { text: 'Receipt To:', style: 'customerTitle' }, + { text: data.customerName, style: 'customerDetails' }, + { text: `Phone: ${data.phoneNumber}`, style: 'customerDetails' }, + { text: `Email: ${data.email}`, style: 'customerDetails' } + ], + margin: [10, 10] + } + ] + ] + }, + layout: 'lightHorizontalLines' + }, + { text: '', margin: [0, 10] }, + + { + table: { + headerRows: 1, + widths: [30, '*', 80, 100], + body: [ + [ + { text: 'No.', style: 'tableHeader', alignment: 'center' }, + { text: 'Description', style: 'tableHeader' }, + { text: 'HSN/SAC', style: 'tableHeader', alignment: 'center' }, + { text: 'Amount (INR)', style: 'tableHeader', alignment: 'right' } + ], + [ + { text: '1', alignment: 'center' }, + { text: `${data.planName} Subscription` }, + { text: '999723', alignment: 'center' }, + { text: baseAmount.toFixed(2), alignment: 'right' } + ] + ] + } + }, + { text: '', margin: [0, 10] }, + + { + columns: [ + { width: '*', text: '' }, + { + width: 'auto', + table: { + widths: [100, 100], + body: hasGst ? [ + [ + { text: 'Taxable Amount:', alignment: 'right' }, + { text: `${baseAmount.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'SGST (9%):', alignment: 'right' }, + { text: `${sgst.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'CGST (9%):', alignment: 'right' }, + { text: `${cgst.toFixed(2)} INR`, alignment: 'right' } + ], + [ + { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, + { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } + ] + ] : [ + [ + { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, + { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } + ] + ] + }, + layout: { + hLineWidth: function(i: number, node: any) { + return (i === node.table.body.length - 1) ? 0.5 : 0; + }, + vLineWidth: function() { return 0; } + } + } + ] + }, + { text: '', margin: [0, 20] }, + + { + style: 'paymentBox', + table: { + widths: ['*'], + body: [ + [ + { + stack: [ + { text: 'Payment Information:', style: 'paymentTitle' }, + { text: `Transaction ID: ${data.transactionId}`, style: 'paymentDetails' }, + { text: `Payment Method: ${data.paymentMethod}`, style: 'paymentDetails' }, + { text: `Payment Date: ${formattedDate}`, style: 'paymentDetails' } + ], + margin: [10, 10] + } + ] + ] + }, + layout: 'lightHorizontalLines' + }, + { text: '', margin: [0, 20] }, + + { text: 'Thank you for your business!', style: 'footer', alignment: 'center' }, + { text: 'This is a computer-generated receipt and does not require a signature.', style: 'disclaimer', alignment: 'center' } + ], + styles: { + businessName: { + fontSize: 20, + bold: true, + margin: [0, 0, 0, 5] + }, + businessAddress: { + fontSize: 12, + margin: [0, 0, 0, 5] + }, + businessDetails: { + fontSize: 12 + }, + invoiceTitle: { + fontSize: 24, + bold: true + }, + invoiceDetails: { + fontSize: 12, + margin: [0, 5, 0, 0] + }, + customerBox: { + margin: [0, 10, 0, 10] + }, + customerTitle: { + fontSize: 12, + bold: true, + margin: [0, 0, 0, 5] + }, + customerDetails: { + fontSize: 12, + margin: [0, 2, 0, 0] + }, + tableHeader: { + fontSize: 12, + bold: true, + margin: [0, 5, 0, 5] + }, + totalAmount: { + fontSize: 12, + bold: true + }, + paymentBox: { + margin: [0, 10, 0, 10] + }, + paymentTitle: { + fontSize: 12, + bold: true, + margin: [0, 0, 0, 5] + }, + paymentDetails: { + fontSize: 12, + margin: [0, 2, 0, 0] + }, + footer: { + fontSize: 12, + italics: true, + margin: [0, 0, 0, 5] + }, + disclaimer: { + fontSize: 10 + } + }, + defaultStyle: { + font: 'Helvetica' + } + }; - // Add customer details - doc.rect(50, 150, 500, 80).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('Receipt To:', 60, 160); - doc.fontSize(12).font('Helvetica').text(data.customerName, 60, 175); - doc.text(`Phone: ${data.phoneNumber}`, 60, 190); - doc.text(`Email: ${data.email}`, 60, 205); + const pdfDoc = pdfMake.createPdf(docDefinition); - // Add table header - const tableTop = 260; - doc.rect(50, tableTop, 500, 30).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('No.', 60, tableTop + 10, { width: 30, align: 'center' }); - doc.text('Description', 100, tableTop + 10, { width: 250 }); - doc.text('HSN/SAC', 350, tableTop + 10, { width: 80, align: 'center' }); - doc.text('Amount (INR)', 430, tableTop + 10, { width: 100, align: 'right' }); - - // Add table row - const rowTop = tableTop + 30; - doc.rect(50, rowTop, 500, 30).stroke(); - doc.fontSize(12).font('Helvetica').text('1', 60, rowTop + 10, { width: 30, align: 'center' }); - doc.text(`${data.planName} Subscription`, 100, rowTop + 10, { width: 250 }); - doc.text('999723', 350, rowTop + 10, { width: 80, align: 'center' }); - doc.text(baseAmount.toFixed(2), 430, rowTop + 10, { width: 100, align: 'right' }); - - // Add totals - let currentY = rowTop + 50; - - if (hasGst) { - doc.fontSize(12).font('Helvetica').text('Taxable Amount:', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${baseAmount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - - doc.text('SGST (9%):', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${sgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - - doc.text('CGST (9%):', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${cgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - currentY += 20; - } - - doc.moveTo(350, currentY).lineTo(550, currentY).stroke(); - currentY += 10; - - doc.fontSize(12).font('Helvetica-Bold').text('Total Amount:', 350, currentY, { width: 100, align: 'right' }); - doc.text(`${data.amount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); - - // Add payment information - currentY += 40; - doc.rect(50, currentY, 500, 80).stroke(); - doc.fontSize(12).font('Helvetica-Bold').text('Payment Information:', 60, currentY + 10); - doc.fontSize(12).font('Helvetica').text(`Transaction ID: ${data.transactionId}`, 60, currentY + 30); - doc.text(`Payment Method: ${data.paymentMethod}`, 60, currentY + 45); - doc.text(`Payment Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 60, currentY + 60); - - // Add footer - currentY += 100; - doc.fontSize(12).font('Helvetica-Oblique').text('Thank you for your business!', 50, currentY, { align: 'center' }); - doc.fontSize(10).font('Helvetica').text('This is a computer-generated receipt and does not require a signature.', 50, currentY + 20, { align: 'center' }); - - // Finalize the PDF - doc.end(); - - // Wait for the file to be written await new Promise((resolve, reject) => { - writeStream.on('finish', () => resolve()); - writeStream.on('error', reject); + pdfDoc.getBuffer((buffer) => { + fs.writeFile(tempFilePath, buffer, (err) => { + if (err) reject(err); + else resolve(); + }); + }); }); - // Upload to Firebase Storage using admin SDK const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { @@ -145,10 +272,8 @@ export class InvoiceService { }, }); - // Clean up the temporary file fs.unlinkSync(tempFilePath); - // Return the storage path return invoicePath; } catch (error: any) { logger.error('Error generating invoice:', error); @@ -158,11 +283,10 @@ export class InvoiceService { async getInvoiceDownloadUrl(invoicePath: string): Promise { try { - // Using admin SDK to generate a signed URL const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); - const expirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days + const expirationMs = 7 * 24 * 60 * 60 * 1000; const [signedUrl] = await file.getSignedUrl({ action: 'read', @@ -193,7 +317,6 @@ export class InvoiceService { const data = docSnapshot.data(); const paymentsData = data?.payments || []; - // Find the payment by referenceNumber or transactionId let found = false; for (let i = 0; i < paymentsData.length; i++) { if (paymentsData[i].referenceNumber === paymentId || @@ -224,15 +347,12 @@ export class InvoiceService { async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { try { - // Get the download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - // Format the date const formattedDate = emailOptions.additionalData?.paymentDate ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') : new Date().toLocaleDateString('en-GB'); - // Create email HTML content if not provided const emailHtml = emailOptions.customHtml || ` @@ -255,7 +375,6 @@ export class InvoiceService { `; - // Send the email with attachment await sendEmailWithAttachmentUtil( emailOptions.recipientEmail, emailOptions.subject || 'Your Fitlien Membership Invoice', @@ -285,10 +404,8 @@ export class InvoiceService { error?: string; }> { try { - // Generate the invoice const invoicePath = await this.generateInvoice(invoiceData); - // Update the payment record with the invoice path const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); if (!updateSuccess) { @@ -300,10 +417,8 @@ export class InvoiceService { }; } - // Get a download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - // Send email if email options are provided let emailSent = false; if (emailOptions && emailOptions.recipientEmail) { emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); From b11cefbab49cdbb8a4bb96993e34b492cfc588bb Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 13:32:51 +0530 Subject: [PATCH 50/81] Update invoiceService.ts --- functions/src/payments/phonepe/invoice/invoiceService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 8f2db0b..eb8a1e5 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -7,7 +7,7 @@ import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; import * as pdfMake from 'pdfmake/build/pdfmake'; import * as pdfFonts from 'pdfmake/build/vfs_fonts'; -(pdfMake as any).vfs = pdfFonts.vfs; +(pdfMake as any).vfs = (pdfFonts as any).pdfMake ? (pdfFonts as any).pdfMake.vfs : pdfFonts.vfs; const admin = getAdmin(); const logger = getLogger(); From 3ef81f8273ed112482a8645a1480a5cc4ab3289a Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 13:32:54 +0530 Subject: [PATCH 51/81] Update index.ts --- functions/src/payments/phonepe/invoice/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/index.ts b/functions/src/payments/phonepe/invoice/index.ts index 3d423aa..daa2183 100644 --- a/functions/src/payments/phonepe/invoice/index.ts +++ b/functions/src/payments/phonepe/invoice/index.ts @@ -2,11 +2,12 @@ import { getInvoiceUrl } from './getInvoiceUrl'; import { InvoiceService } from './invoiceService'; import { processInvoice } from './processInvoice'; import { sendInvoiceEmail } from './sendInvoiceEmail'; +import { directGenerateInvoice } from './directInvoice'; -// Export all invoice-related functions export { getInvoiceUrl, InvoiceService, processInvoice, - sendInvoiceEmail + sendInvoiceEmail, + directGenerateInvoice, }; From ae19a611b37a1bc20f6291da2fbab45797cd0465 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 08:04:46 +0000 Subject: [PATCH 52/81] phonepe (#30) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/30 --- functions/src/payments/phonepe/invoice/index.ts | 5 +++-- functions/src/payments/phonepe/invoice/invoiceService.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/index.ts b/functions/src/payments/phonepe/invoice/index.ts index 3d423aa..daa2183 100644 --- a/functions/src/payments/phonepe/invoice/index.ts +++ b/functions/src/payments/phonepe/invoice/index.ts @@ -2,11 +2,12 @@ import { getInvoiceUrl } from './getInvoiceUrl'; import { InvoiceService } from './invoiceService'; import { processInvoice } from './processInvoice'; import { sendInvoiceEmail } from './sendInvoiceEmail'; +import { directGenerateInvoice } from './directInvoice'; -// Export all invoice-related functions export { getInvoiceUrl, InvoiceService, processInvoice, - sendInvoiceEmail + sendInvoiceEmail, + directGenerateInvoice, }; diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 8f2db0b..eb8a1e5 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -7,7 +7,7 @@ import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; import * as pdfMake from 'pdfmake/build/pdfmake'; import * as pdfFonts from 'pdfmake/build/vfs_fonts'; -(pdfMake as any).vfs = pdfFonts.vfs; +(pdfMake as any).vfs = (pdfFonts as any).pdfMake ? (pdfFonts as any).pdfMake.vfs : pdfFonts.vfs; const admin = getAdmin(); const logger = getLogger(); From d8dfd8a6f27acc29c49dfdfa972f6838ba206ba6 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 13:50:04 +0530 Subject: [PATCH 53/81] pdf changes --- .../payments/phonepe/invoice/invoiceService.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index eb8a1e5..c1edf9b 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -4,10 +4,18 @@ import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; -import * as pdfMake from 'pdfmake/build/pdfmake'; -import * as pdfFonts from 'pdfmake/build/vfs_fonts'; -(pdfMake as any).vfs = (pdfFonts as any).pdfMake ? (pdfFonts as any).pdfMake.vfs : pdfFonts.vfs; +let pdfMake: any; +let pdfFonts: any; + +function initPdfLibraries() { + if (!pdfMake) { + pdfMake = require('pdfmake/build/pdfmake'); + pdfFonts = require('pdfmake/build/vfs_fonts'); + pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; + } + return pdfMake; +} const admin = getAdmin(); const logger = getLogger(); @@ -45,6 +53,7 @@ export interface EmailOptions { export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { + const pdfMake = initPdfLibraries(); const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); const hasGst = data.gstNumber && data.gstNumber.length > 0; @@ -255,7 +264,7 @@ export class InvoiceService { const pdfDoc = pdfMake.createPdf(docDefinition); await new Promise((resolve, reject) => { - pdfDoc.getBuffer((buffer) => { + pdfDoc.getBuffer((buffer: Buffer) => { fs.writeFile(tempFilePath, buffer, (err) => { if (err) reject(err); else resolve(); From 8bb10cba8ca0c8b6e3e8cc2622dcc8e3427789e5 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 08:22:02 +0000 Subject: [PATCH 54/81] phonepe (#31) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/31 --- .../payments/phonepe/invoice/invoiceService.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index eb8a1e5..c1edf9b 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -4,10 +4,18 @@ import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; -import * as pdfMake from 'pdfmake/build/pdfmake'; -import * as pdfFonts from 'pdfmake/build/vfs_fonts'; -(pdfMake as any).vfs = (pdfFonts as any).pdfMake ? (pdfFonts as any).pdfMake.vfs : pdfFonts.vfs; +let pdfMake: any; +let pdfFonts: any; + +function initPdfLibraries() { + if (!pdfMake) { + pdfMake = require('pdfmake/build/pdfmake'); + pdfFonts = require('pdfmake/build/vfs_fonts'); + pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; + } + return pdfMake; +} const admin = getAdmin(); const logger = getLogger(); @@ -45,6 +53,7 @@ export interface EmailOptions { export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { + const pdfMake = initPdfLibraries(); const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); const hasGst = data.gstNumber && data.gstNumber.length > 0; @@ -255,7 +264,7 @@ export class InvoiceService { const pdfDoc = pdfMake.createPdf(docDefinition); await new Promise((resolve, reject) => { - pdfDoc.getBuffer((buffer) => { + pdfDoc.getBuffer((buffer: Buffer) => { fs.writeFile(tempFilePath, buffer, (err) => { if (err) reject(err); else resolve(); From 762e6b77e5ed5516555490688a24a1a383437565 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 14:15:36 +0530 Subject: [PATCH 55/81] Update invoiceService.ts --- .../src/payments/phonepe/invoice/invoiceService.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index c1edf9b..59bb0fd 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -5,17 +5,10 @@ import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; -let pdfMake: any; -let pdfFonts: any; +const pdfMake = require('pdfmake/build/pdfmake'); +const pdfFonts = require('pdfmake/build/vfs_fonts'); +pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; -function initPdfLibraries() { - if (!pdfMake) { - pdfMake = require('pdfmake/build/pdfmake'); - pdfFonts = require('pdfmake/build/vfs_fonts'); - pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; - } - return pdfMake; -} const admin = getAdmin(); const logger = getLogger(); @@ -53,7 +46,6 @@ export interface EmailOptions { export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { - const pdfMake = initPdfLibraries(); const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); const hasGst = data.gstNumber && data.gstNumber.length > 0; From 815e15b5aedc550148d4ef7549dfa83b32dcab6b Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 08:47:28 +0000 Subject: [PATCH 56/81] phonepe (#32) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/32 --- .../src/payments/phonepe/invoice/invoiceService.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index c1edf9b..59bb0fd 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -5,17 +5,10 @@ import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; -let pdfMake: any; -let pdfFonts: any; +const pdfMake = require('pdfmake/build/pdfmake'); +const pdfFonts = require('pdfmake/build/vfs_fonts'); +pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; -function initPdfLibraries() { - if (!pdfMake) { - pdfMake = require('pdfmake/build/pdfmake'); - pdfFonts = require('pdfmake/build/vfs_fonts'); - pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; - } - return pdfMake; -} const admin = getAdmin(); const logger = getLogger(); @@ -53,7 +46,6 @@ export interface EmailOptions { export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { - const pdfMake = initPdfLibraries(); const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); const hasGst = data.gstNumber && data.gstNumber.length > 0; From 06c5f018c316536236bba9847bd74052a86b30f4 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 14:32:59 +0530 Subject: [PATCH 57/81] changed pdf again --- functions/package-lock.json | 210 ++++++++++++ functions/package.json | 2 + .../phonepe/invoice/invoiceService.ts | 298 +++++------------- 3 files changed, 299 insertions(+), 211 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 9feb2ad..e5b23b4 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -18,6 +18,8 @@ "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", @@ -1101,6 +1103,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", @@ -2859,6 +2869,12 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -2924,6 +2940,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "optional": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3066,6 +3088,17 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3248,6 +3281,15 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3379,6 +3421,17 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -3494,6 +3547,25 @@ ], "peer": true }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3659,6 +3731,17 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "hasInstallScript": true, + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3713,6 +3796,15 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -3888,6 +3980,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -4269,6 +4370,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4833,6 +4939,19 @@ "node": ">=14" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -6047,6 +6166,31 @@ "node": ">=10" } }, + "node_modules/jspdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", + "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "dependencies": { + "@babel/runtime": "^7.26.7", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", + "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", + "peerDependencies": { + "jspdf": "^2 || ^3" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -6765,6 +6909,12 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6960,6 +7110,15 @@ "node": ">=0.4.x" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7003,6 +7162,12 @@ "node": ">= 6" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7108,6 +7273,15 @@ "node": ">=14" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7431,6 +7605,15 @@ "node": ">=10" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7579,6 +7762,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/teeny-request": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", @@ -7671,6 +7863,15 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -7931,6 +8132,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/functions/package.json b/functions/package.json index bbfdd3f..b9f1c89 100644 --- a/functions/package.json +++ b/functions/package.json @@ -25,6 +25,8 @@ "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 59bb0fd..95ffa7d 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -4,12 +4,8 @@ import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; - -const pdfMake = require('pdfmake/build/pdfmake'); -const pdfFonts = require('pdfmake/build/vfs_fonts'); -pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; - - +import { jsPDF } from "jspdf"; +import 'jspdf-autotable'; const admin = getAdmin(); const logger = getLogger(); @@ -54,216 +50,96 @@ export class InvoiceService { const cgst = hasGst ? baseAmount * 0.09 : 0; const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); + const doc = new jsPDF(); - const docDefinition: any = { - content: [ - { - columns: [ - [ - { text: data.businessName, style: 'businessName' }, - { text: data.address, style: 'businessAddress' }, - hasGst ? { text: `GSTIN: ${data.gstNumber}`, style: 'businessDetails' } : {} - ], - [ - { text: 'RECEIPT', style: 'invoiceTitle', alignment: 'right' }, - { text: `Receipt #: ${data.invoiceNumber}`, style: 'invoiceDetails', alignment: 'right' }, - { text: `Date: ${formattedDate}`, style: 'invoiceDetails', alignment: 'right' } - ] - ] - }, - { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 0.5 }] }, - { text: '', margin: [0, 10] }, - - { - style: 'customerBox', - table: { - widths: ['*'], - body: [ - [ - { - stack: [ - { text: 'Receipt To:', style: 'customerTitle' }, - { text: data.customerName, style: 'customerDetails' }, - { text: `Phone: ${data.phoneNumber}`, style: 'customerDetails' }, - { text: `Email: ${data.email}`, style: 'customerDetails' } - ], - margin: [10, 10] - } - ] - ] - }, - layout: 'lightHorizontalLines' - }, - { text: '', margin: [0, 10] }, - - { - table: { - headerRows: 1, - widths: [30, '*', 80, 100], - body: [ - [ - { text: 'No.', style: 'tableHeader', alignment: 'center' }, - { text: 'Description', style: 'tableHeader' }, - { text: 'HSN/SAC', style: 'tableHeader', alignment: 'center' }, - { text: 'Amount (INR)', style: 'tableHeader', alignment: 'right' } - ], - [ - { text: '1', alignment: 'center' }, - { text: `${data.planName} Subscription` }, - { text: '999723', alignment: 'center' }, - { text: baseAmount.toFixed(2), alignment: 'right' } - ] - ] - } - }, - { text: '', margin: [0, 10] }, - - { - columns: [ - { width: '*', text: '' }, - { - width: 'auto', - table: { - widths: [100, 100], - body: hasGst ? [ - [ - { text: 'Taxable Amount:', alignment: 'right' }, - { text: `${baseAmount.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'SGST (9%):', alignment: 'right' }, - { text: `${sgst.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'CGST (9%):', alignment: 'right' }, - { text: `${cgst.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, - { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } - ] - ] : [ - [ - { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, - { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } - ] - ] - }, - layout: { - hLineWidth: function(i: number, node: any) { - return (i === node.table.body.length - 1) ? 0.5 : 0; - }, - vLineWidth: function() { return 0; } - } - } - ] - }, - { text: '', margin: [0, 20] }, - - { - style: 'paymentBox', - table: { - widths: ['*'], - body: [ - [ - { - stack: [ - { text: 'Payment Information:', style: 'paymentTitle' }, - { text: `Transaction ID: ${data.transactionId}`, style: 'paymentDetails' }, - { text: `Payment Method: ${data.paymentMethod}`, style: 'paymentDetails' }, - { text: `Payment Date: ${formattedDate}`, style: 'paymentDetails' } - ], - margin: [10, 10] - } - ] - ] - }, - layout: 'lightHorizontalLines' - }, - { text: '', margin: [0, 20] }, - - { text: 'Thank you for your business!', style: 'footer', alignment: 'center' }, - { text: 'This is a computer-generated receipt and does not require a signature.', style: 'disclaimer', alignment: 'center' } + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text(data.businessName, 20, 20); + + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + doc.text(data.address, 20, 30); + + if (hasGst) { + doc.text(`GSTIN: ${data.gstNumber}`, 20, 40); + } + + doc.setFontSize(24); + doc.setFont('helvetica', 'bold'); + doc.text('RECEIPT', 190, 20, { align: 'right' }); + + doc.setFontSize(12); + doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); + doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); + + doc.line(20, 45, 190, 45); + + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Receipt To:', 20, 60); + + doc.setFont('helvetica', 'normal'); + doc.text(data.customerName, 20, 70); + doc.text(`Phone: ${data.phoneNumber}`, 20, 80); + doc.text(`Email: ${data.email}`, 20, 90); + + (doc as any).autoTable({ + startY: 110, + head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], + body: [ + ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], - styles: { - businessName: { - fontSize: 20, - bold: true, - margin: [0, 0, 0, 5] - }, - businessAddress: { - fontSize: 12, - margin: [0, 0, 0, 5] - }, - businessDetails: { - fontSize: 12 - }, - invoiceTitle: { - fontSize: 24, - bold: true - }, - invoiceDetails: { - fontSize: 12, - margin: [0, 5, 0, 0] - }, - customerBox: { - margin: [0, 10, 0, 10] - }, - customerTitle: { - fontSize: 12, - bold: true, - margin: [0, 0, 0, 5] - }, - customerDetails: { - fontSize: 12, - margin: [0, 2, 0, 0] - }, - tableHeader: { - fontSize: 12, - bold: true, - margin: [0, 5, 0, 5] - }, - totalAmount: { - fontSize: 12, - bold: true - }, - paymentBox: { - margin: [0, 10, 0, 10] - }, - paymentTitle: { - fontSize: 12, - bold: true, - margin: [0, 0, 0, 5] - }, - paymentDetails: { - fontSize: 12, - margin: [0, 2, 0, 0] - }, - footer: { - fontSize: 12, - italics: true, - margin: [0, 0, 0, 5] - }, - disclaimer: { - fontSize: 10 - } - }, - defaultStyle: { - font: 'Helvetica' + headStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: 'bold' }, + styles: { halign: 'center' }, + columnStyles: { + 0: { halign: 'center', cellWidth: 20 }, + 1: { halign: 'left' }, + 2: { halign: 'center', cellWidth: 40 }, + 3: { halign: 'right', cellWidth: 40 } } - }; - - const pdfDoc = pdfMake.createPdf(docDefinition); - - await new Promise((resolve, reject) => { - pdfDoc.getBuffer((buffer: Buffer) => { - fs.writeFile(tempFilePath, buffer, (err) => { - if (err) reject(err); - else resolve(); - }); - }); }); + const finalY = (doc as any).lastAutoTable.finalY + 20; + + if (hasGst) { + doc.text('Taxable Amount:', 150, finalY, { align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + + doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' }); + doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' }); + + doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); + doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); + + doc.setFont('helvetica', 'bold'); + doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' }); + } else { + doc.setFont('helvetica', 'bold'); + doc.text('Total Amount:', 150, finalY, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + } + + const paymentY = hasGst ? finalY + 50 : finalY + 20; + + doc.setFont('helvetica', 'bold'); + doc.text('Payment Information:', 20, paymentY); + + doc.setFont('helvetica', 'normal'); + doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10); + doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20); + doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); + + doc.setFontSize(12); + doc.setFont('helvetica', 'italic'); + doc.text('Thank you for your business!', 105, 270, { align: 'center' }); + + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' }); + + fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer'))); + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { From b372d81ff04e4ec0b1e92da0db3c1895f315d1b4 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 09:04:45 +0000 Subject: [PATCH 58/81] phonepe (#33) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/33 --- functions/package-lock.json | 210 ++++++++++++ functions/package.json | 2 + .../phonepe/invoice/invoiceService.ts | 298 +++++------------- 3 files changed, 299 insertions(+), 211 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 9feb2ad..e5b23b4 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -18,6 +18,8 @@ "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", @@ -1101,6 +1103,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", @@ -2859,6 +2869,12 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -2924,6 +2940,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "optional": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3066,6 +3088,17 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3248,6 +3281,15 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3379,6 +3421,17 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -3494,6 +3547,25 @@ ], "peer": true }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3659,6 +3731,17 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "hasInstallScript": true, + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3713,6 +3796,15 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -3888,6 +3980,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -4269,6 +4370,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4833,6 +4939,19 @@ "node": ">=14" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -6047,6 +6166,31 @@ "node": ">=10" } }, + "node_modules/jspdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", + "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "dependencies": { + "@babel/runtime": "^7.26.7", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", + "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", + "peerDependencies": { + "jspdf": "^2 || ^3" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -6765,6 +6909,12 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6960,6 +7110,15 @@ "node": ">=0.4.x" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7003,6 +7162,12 @@ "node": ">= 6" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7108,6 +7273,15 @@ "node": ">=14" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7431,6 +7605,15 @@ "node": ">=10" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7579,6 +7762,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/teeny-request": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", @@ -7671,6 +7863,15 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -7931,6 +8132,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/functions/package.json b/functions/package.json index bbfdd3f..b9f1c89 100644 --- a/functions/package.json +++ b/functions/package.json @@ -25,6 +25,8 @@ "form-data": "^4.0.1", "functions": "file:", "html-to-text": "^9.0.5", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "long": "^5.3.2", "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 59bb0fd..95ffa7d 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -4,12 +4,8 @@ import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; - -const pdfMake = require('pdfmake/build/pdfmake'); -const pdfFonts = require('pdfmake/build/vfs_fonts'); -pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; - - +import { jsPDF } from "jspdf"; +import 'jspdf-autotable'; const admin = getAdmin(); const logger = getLogger(); @@ -54,216 +50,96 @@ export class InvoiceService { const cgst = hasGst ? baseAmount * 0.09 : 0; const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); + const doc = new jsPDF(); - const docDefinition: any = { - content: [ - { - columns: [ - [ - { text: data.businessName, style: 'businessName' }, - { text: data.address, style: 'businessAddress' }, - hasGst ? { text: `GSTIN: ${data.gstNumber}`, style: 'businessDetails' } : {} - ], - [ - { text: 'RECEIPT', style: 'invoiceTitle', alignment: 'right' }, - { text: `Receipt #: ${data.invoiceNumber}`, style: 'invoiceDetails', alignment: 'right' }, - { text: `Date: ${formattedDate}`, style: 'invoiceDetails', alignment: 'right' } - ] - ] - }, - { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 0.5 }] }, - { text: '', margin: [0, 10] }, - - { - style: 'customerBox', - table: { - widths: ['*'], - body: [ - [ - { - stack: [ - { text: 'Receipt To:', style: 'customerTitle' }, - { text: data.customerName, style: 'customerDetails' }, - { text: `Phone: ${data.phoneNumber}`, style: 'customerDetails' }, - { text: `Email: ${data.email}`, style: 'customerDetails' } - ], - margin: [10, 10] - } - ] - ] - }, - layout: 'lightHorizontalLines' - }, - { text: '', margin: [0, 10] }, - - { - table: { - headerRows: 1, - widths: [30, '*', 80, 100], - body: [ - [ - { text: 'No.', style: 'tableHeader', alignment: 'center' }, - { text: 'Description', style: 'tableHeader' }, - { text: 'HSN/SAC', style: 'tableHeader', alignment: 'center' }, - { text: 'Amount (INR)', style: 'tableHeader', alignment: 'right' } - ], - [ - { text: '1', alignment: 'center' }, - { text: `${data.planName} Subscription` }, - { text: '999723', alignment: 'center' }, - { text: baseAmount.toFixed(2), alignment: 'right' } - ] - ] - } - }, - { text: '', margin: [0, 10] }, - - { - columns: [ - { width: '*', text: '' }, - { - width: 'auto', - table: { - widths: [100, 100], - body: hasGst ? [ - [ - { text: 'Taxable Amount:', alignment: 'right' }, - { text: `${baseAmount.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'SGST (9%):', alignment: 'right' }, - { text: `${sgst.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'CGST (9%):', alignment: 'right' }, - { text: `${cgst.toFixed(2)} INR`, alignment: 'right' } - ], - [ - { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, - { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } - ] - ] : [ - [ - { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, - { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } - ] - ] - }, - layout: { - hLineWidth: function(i: number, node: any) { - return (i === node.table.body.length - 1) ? 0.5 : 0; - }, - vLineWidth: function() { return 0; } - } - } - ] - }, - { text: '', margin: [0, 20] }, - - { - style: 'paymentBox', - table: { - widths: ['*'], - body: [ - [ - { - stack: [ - { text: 'Payment Information:', style: 'paymentTitle' }, - { text: `Transaction ID: ${data.transactionId}`, style: 'paymentDetails' }, - { text: `Payment Method: ${data.paymentMethod}`, style: 'paymentDetails' }, - { text: `Payment Date: ${formattedDate}`, style: 'paymentDetails' } - ], - margin: [10, 10] - } - ] - ] - }, - layout: 'lightHorizontalLines' - }, - { text: '', margin: [0, 20] }, - - { text: 'Thank you for your business!', style: 'footer', alignment: 'center' }, - { text: 'This is a computer-generated receipt and does not require a signature.', style: 'disclaimer', alignment: 'center' } + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text(data.businessName, 20, 20); + + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + doc.text(data.address, 20, 30); + + if (hasGst) { + doc.text(`GSTIN: ${data.gstNumber}`, 20, 40); + } + + doc.setFontSize(24); + doc.setFont('helvetica', 'bold'); + doc.text('RECEIPT', 190, 20, { align: 'right' }); + + doc.setFontSize(12); + doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); + doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); + + doc.line(20, 45, 190, 45); + + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Receipt To:', 20, 60); + + doc.setFont('helvetica', 'normal'); + doc.text(data.customerName, 20, 70); + doc.text(`Phone: ${data.phoneNumber}`, 20, 80); + doc.text(`Email: ${data.email}`, 20, 90); + + (doc as any).autoTable({ + startY: 110, + head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], + body: [ + ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], - styles: { - businessName: { - fontSize: 20, - bold: true, - margin: [0, 0, 0, 5] - }, - businessAddress: { - fontSize: 12, - margin: [0, 0, 0, 5] - }, - businessDetails: { - fontSize: 12 - }, - invoiceTitle: { - fontSize: 24, - bold: true - }, - invoiceDetails: { - fontSize: 12, - margin: [0, 5, 0, 0] - }, - customerBox: { - margin: [0, 10, 0, 10] - }, - customerTitle: { - fontSize: 12, - bold: true, - margin: [0, 0, 0, 5] - }, - customerDetails: { - fontSize: 12, - margin: [0, 2, 0, 0] - }, - tableHeader: { - fontSize: 12, - bold: true, - margin: [0, 5, 0, 5] - }, - totalAmount: { - fontSize: 12, - bold: true - }, - paymentBox: { - margin: [0, 10, 0, 10] - }, - paymentTitle: { - fontSize: 12, - bold: true, - margin: [0, 0, 0, 5] - }, - paymentDetails: { - fontSize: 12, - margin: [0, 2, 0, 0] - }, - footer: { - fontSize: 12, - italics: true, - margin: [0, 0, 0, 5] - }, - disclaimer: { - fontSize: 10 - } - }, - defaultStyle: { - font: 'Helvetica' + headStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: 'bold' }, + styles: { halign: 'center' }, + columnStyles: { + 0: { halign: 'center', cellWidth: 20 }, + 1: { halign: 'left' }, + 2: { halign: 'center', cellWidth: 40 }, + 3: { halign: 'right', cellWidth: 40 } } - }; - - const pdfDoc = pdfMake.createPdf(docDefinition); - - await new Promise((resolve, reject) => { - pdfDoc.getBuffer((buffer: Buffer) => { - fs.writeFile(tempFilePath, buffer, (err) => { - if (err) reject(err); - else resolve(); - }); - }); }); + const finalY = (doc as any).lastAutoTable.finalY + 20; + + if (hasGst) { + doc.text('Taxable Amount:', 150, finalY, { align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + + doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' }); + doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' }); + + doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); + doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); + + doc.setFont('helvetica', 'bold'); + doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' }); + } else { + doc.setFont('helvetica', 'bold'); + doc.text('Total Amount:', 150, finalY, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + } + + const paymentY = hasGst ? finalY + 50 : finalY + 20; + + doc.setFont('helvetica', 'bold'); + doc.text('Payment Information:', 20, paymentY); + + doc.setFont('helvetica', 'normal'); + doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10); + doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20); + doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); + + doc.setFontSize(12); + doc.setFont('helvetica', 'italic'); + doc.text('Thank you for your business!', 105, 270, { align: 'center' }); + + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' }); + + fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer'))); + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { From 9c002a4d616de3f7844ee9b0692bc958aa975dea Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 14:41:50 +0530 Subject: [PATCH 59/81] Update firebase.json --- firebase.json | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase.json b/firebase.json index dfc4226..ecc3d3c 100644 --- a/firebase.json +++ b/firebase.json @@ -7,6 +7,7 @@ { "source": "functions", "codebase": "default", + "timeoutSeconds": 300, "ignore": [ "node_modules", ".git", From 92320f166add1e08bc726e055158012cf3fcc7b9 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 09:14:04 +0000 Subject: [PATCH 60/81] phonepe (#34) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/34 --- firebase.json | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase.json b/firebase.json index dfc4226..ecc3d3c 100644 --- a/firebase.json +++ b/firebase.json @@ -7,6 +7,7 @@ { "source": "functions", "codebase": "default", + "timeoutSeconds": 300, "ignore": [ "node_modules", ".git", From c6f8b7fc351a38cc9c750a7dc7947832c6b7b89e Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 15:19:01 +0530 Subject: [PATCH 61/81] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 05e4ee9..d392302 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -106,6 +106,15 @@ export const phonePeWebhook = onRequest({ } if (payload.state === 'COMPLETED') { + logger.info(`Payment COMPLETED for merchantOrderId: ${payload.merchantOrderId}, orderId: ${payload.orderId}`); + + logger.info('Payment completion details:', { + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + amount: payload.amount, + paymentInstrument: payload.paymentInstrument, + timestamp: new Date().toISOString() + }); try { const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( payload.merchantOrderId, From a4c3cbbba28fd414c838e69558d817fd21b6663b Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 15:40:30 +0530 Subject: [PATCH 62/81] added logging --- functions/src/payments/phonepe/webhook.ts | 72 +++++++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index d392302..1694817 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -21,7 +21,7 @@ export const phonePeWebhook = onRequest({ body: request.body, method: request.method }); - + const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; @@ -37,7 +37,7 @@ export const phonePeWebhook = onRequest({ .createHash('sha256') .update(credentialString) .digest('hex'); - + const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { @@ -106,44 +106,54 @@ export const phonePeWebhook = onRequest({ } if (payload.state === 'COMPLETED') { - logger.info(`Payment COMPLETED for merchantOrderId: ${payload.merchantOrderId}, orderId: ${payload.orderId}`); - - logger.info('Payment completion details:', { - merchantOrderId: payload.merchantOrderId, - orderId: payload.orderId, - amount: payload.amount, - paymentInstrument: payload.paymentInstrument, - timestamp: new Date().toISOString() - }); try { + logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`); + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( payload.merchantOrderId, payload.orderId, payload ); + logger.info(`Payment update result for merchantOrderId: ${payload.merchantOrderId}`, { + success: paymentUpdateSuccess, + orderId: payload.orderId + }); + if (paymentUpdateSuccess) { const orderData = orderDoc.data(); - const membershipId = orderData.metaInfo?.membershipId; + logger.info(`Processing invoice for completed payment`, { + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + membershipId: membershipId || 'not-provided' + }); + if (membershipId) { try { + logger.info(`Fetching membership data for membershipId: ${membershipId}`); const membershipDoc = await admin.firestore() .collection('memberships') .doc(membershipId) .get(); if (membershipDoc.exists) { + logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`); + const membershipData = membershipDoc.data(); const userId = membershipData?.userId; + logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() .collection('users') .doc(userId) .get(); - if (userDoc.exists) { + logger.info(`User data retrieved successfully for userId: ${userId}`); + + logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); + const userData = userDoc.data(); const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; @@ -211,6 +221,14 @@ export const phonePeWebhook = onRequest({ } const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; + + logger.info(`Generated invoice number: ${invoiceNumber}`); + + logger.info(`Preparing invoice data for generation`, { + invoiceNumber, + merchantOrderId: payload.merchantOrderId, + gymName: gymName + }); const invoiceData = { invoiceNumber, businessName: gymName, @@ -227,6 +245,12 @@ export const phonePeWebhook = onRequest({ }; const invoicePath = await invoiceService.generateInvoice(invoiceData); + logger.info(`Invoice generated successfully at path: ${invoicePath}`); + + logger.info(`Updating membership payment with invoice path`, { + membershipId, + invoicePath + }); await admin.firestore() .collection('membership_payments') @@ -234,29 +258,50 @@ export const phonePeWebhook = onRequest({ .get() .then(async (doc) => { if (doc.exists) { + logger.info(`Found membership payment document for membershipId: ${membershipId}`); + const paymentsData = doc.data()?.payments || []; + let paymentFound = false; + for (let i = 0; i < paymentsData.length; i++) { if (paymentsData[i].referenceNumber === payload.merchantOrderId || paymentsData[i].transactionId === payload.orderId) { paymentsData[i].invoicePath = invoicePath; + paymentFound = true; break; } } + logger.info(`Payment record ${paymentFound ? 'found' : 'not found'} in membership payments`, { + membershipId, + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId + }); + await doc.ref.update({ 'payments': paymentsData, 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), }); + + logger.info(`Successfully updated membership payment with invoice path`, { + membershipId, + invoicePath + }); + } else { + logger.warn(`No membership payment document found for membershipId: ${membershipId}`); } }); logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`); + logger.info(`Getting download URL for invoice: ${invoicePath}`); const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + logger.info(`Generated download URL for invoice: ${invoicePath}`); const formattedDate = format(new Date(), 'dd/MM/yyyy'); if (membershipData?.fields?.['email']) { + logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`); try { const emailSubject = isFreeplan ? `Free Plan Assigned - ${gymName}` @@ -303,6 +348,7 @@ export const phonePeWebhook = onRequest({ } if (gymOwnerEmail) { + logger.info(`Preparing to send invoice email to gym owner: ${gymOwnerEmail}`); try { const ownerEmailSubject = isFreeplan ? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}` From ba54655b8c8daf5c8c15c55fc650902104a69801 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 10:16:12 +0000 Subject: [PATCH 63/81] Update firebase.json (#36) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/36 --- firebase.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase.json b/firebase.json index ecc3d3c..6920866 100644 --- a/firebase.json +++ b/firebase.json @@ -7,7 +7,7 @@ { "source": "functions", "codebase": "default", - "timeoutSeconds": 300, + "timeoutSeconds": 360, "ignore": [ "node_modules", ".git", From 038d85708f799950d93a4f39149f236890ebdc07 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 10:40:05 +0000 Subject: [PATCH 64/81] phonepe (#37) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/37 --- functions/src/payments/phonepe/webhook.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 1694817..90a0a43 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -105,7 +105,15 @@ export const phonePeWebhook = onRequest({ logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } - if (payload.state === 'COMPLETED') { + logger.info(`Checking payment state`, { + state: payload.state, + stateType: typeof payload.state, + stateLength: payload.state ? payload.state.length : 0, + stateUpperCase: payload.state ? payload.state.toUpperCase() : null, + stateComparison: payload.state === 'COMPLETED' + }); + + if (payload.state && payload.state.trim().toUpperCase() === 'COMPLETED') { try { logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`); From 271858a4d3bbb8294c5d31429b3f8b47111793be Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 10:47:25 +0000 Subject: [PATCH 65/81] phonepe (#38) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/38 --- firebase.json | 1 - functions/src/index.ts | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/firebase.json b/firebase.json index 6920866..dfc4226 100644 --- a/firebase.json +++ b/firebase.json @@ -7,7 +7,6 @@ { "source": "functions", "codebase": "default", - "timeoutSeconds": 360, "ignore": [ "node_modules", ".git", diff --git a/functions/src/index.ts b/functions/src/index.ts index 19fa4e0..8bc48e9 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,3 +6,6 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; +import { setGlobalOptions } from "firebase-functions/v2"; + +setGlobalOptions({ timeoutSeconds: 300 }); \ No newline at end of file From fe7839b899c5bfbeaa0f497ee2da4f1ae3671bfa Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 10:53:11 +0000 Subject: [PATCH 66/81] phonepe (#39) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/39 --- functions/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 8bc48e9..1b8cb5b 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,4 +8,4 @@ export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; import { setGlobalOptions } from "firebase-functions/v2"; -setGlobalOptions({ timeoutSeconds: 300 }); \ No newline at end of file +setGlobalOptions({ timeoutSeconds: 400 }); From e382ca37896b7ae9390d74be65f7e4d0e963fce3 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 20 May 2025 10:59:07 +0000 Subject: [PATCH 67/81] phonepe (#40) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/40 --- firebase.json | 1 + functions/src/index.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/firebase.json b/firebase.json index dfc4226..608886c 100644 --- a/firebase.json +++ b/firebase.json @@ -7,6 +7,7 @@ { "source": "functions", "codebase": "default", + "timeoutSeconds": 540, "ignore": [ "node_modules", ".git", diff --git a/functions/src/index.ts b/functions/src/index.ts index 1b8cb5b..19fa4e0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,6 +6,3 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; -import { setGlobalOptions } from "firebase-functions/v2"; - -setGlobalOptions({ timeoutSeconds: 400 }); From e53310b4cff55c586aa4012632f79521b64033e8 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 21 May 2025 08:43:39 +0000 Subject: [PATCH 68/81] phonepe (#41) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/41 --- firebase.json | 1 + functions/package-lock.json | 10 ++++++++++ functions/package.json | 1 + functions/src/index.ts | 11 +++++++++++ package.json | 6 +----- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/firebase.json b/firebase.json index 608886c..21d0f5f 100644 --- a/firebase.json +++ b/firebase.json @@ -8,6 +8,7 @@ "source": "functions", "codebase": "default", "timeoutSeconds": 540, + "memory": "1GiB", "ignore": [ "node_modules", ".git", diff --git a/functions/package-lock.json b/functions/package-lock.json index e5b23b4..149bfc8 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -13,6 +13,7 @@ "aws-sdk": "^2.1692.0", "axios": "^1.9.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", @@ -3805,6 +3806,15 @@ "utrie": "^1.0.2" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", diff --git a/functions/package.json b/functions/package.json index b9f1c89..0e77a8f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -20,6 +20,7 @@ "aws-sdk": "^2.1692.0", "axios": "^1.9.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/src/index.ts b/functions/src/index.ts index 19fa4e0..660ddc7 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,3 +1,14 @@ +import { setGlobalOptions } from "firebase-functions/v2"; + +setGlobalOptions({ + region: "#{SERVICES_RGN}#", + memory: "1GiB", + timeoutSeconds: 540, + minInstances: 0, + maxInstances: 10, + concurrency: 80 +}); + export * from './shared/config'; export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email'; export { sendSMSMessage } from './sms'; diff --git a/package.json b/package.json index 073e12e..da0eb6c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,7 @@ { "dependencies": { "@types/busboy": "^1.5.4", - "@types/nodemailer": "^6.4.17", - "@types/pdfkit": "^0.13.9", "busboy": "^1.6.0", - "date-fns": "^4.1.0", - "nodemailer": "^7.0.3", - "pdfkit": "^0.17.1" + "date-fns": "^4.1.0" } } From 4b054fac56cde2db90f7dcf4b0256381c4031561 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 21 May 2025 10:10:13 +0000 Subject: [PATCH 69/81] phonepe (#42) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/42 --- functions/src/payments/phonepe/invoice/invoiceService.ts | 4 ++-- functions/src/payments/phonepe/webhook.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 95ffa7d..8c3d491 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; import { jsPDF } from "jspdf"; -import 'jspdf-autotable'; +import autoTable from 'jspdf-autotable'; const admin = getAdmin(); const logger = getLogger(); @@ -83,7 +83,7 @@ export class InvoiceService { doc.text(`Phone: ${data.phoneNumber}`, 20, 80); doc.text(`Email: ${data.email}`, 20, 90); - (doc as any).autoTable({ + autoTable(doc,{ startY: 110, head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], body: [ diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 90a0a43..42aa29f 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -154,7 +154,7 @@ export const phonePeWebhook = onRequest({ logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() - .collection('users') + .collection('client_profiles') .doc(userId) .get(); if (userDoc.exists) { From d3cc6b3710f23438b37c38b0b48bdb5d0309f306 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 21 May 2025 11:31:44 +0000 Subject: [PATCH 70/81] phonepe (#43) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/43 --- .../payments/phonepe/invoice/directInvoice.ts | 2 - .../payments/phonepe/invoice/getInvoiceUrl.ts | 1 - .../phonepe/invoice/invoiceService.ts | 165 ++++++++++++------ functions/src/payments/phonepe/paymentData.ts | 16 +- 4 files changed, 114 insertions(+), 70 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/directInvoice.ts b/functions/src/payments/phonepe/invoice/directInvoice.ts index 93969f5..5df728d 100644 --- a/functions/src/payments/phonepe/invoice/directInvoice.ts +++ b/functions/src/payments/phonepe/invoice/directInvoice.ts @@ -65,11 +65,9 @@ export const directGenerateInvoice = onRequest({ paymentMethod: paymentMethod || 'Online' }; - // Generate the invoice without updating any payment records const invoicePath = await invoiceService.generateInvoice(invoiceData); const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); - // Send email if requested let emailSent = false; if (sendEmail && email) { emailSent = await invoiceService.sendInvoiceEmail(invoicePath, { diff --git a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts index 038a14c..df433f4 100644 --- a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts +++ b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts @@ -35,7 +35,6 @@ export const getInvoiceUrl = onRequest({ return; } - // Get a download URL for the invoice const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); response.json({ diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 8c3d491..bcc78a4 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -43,7 +43,7 @@ export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); - + const hasGst = data.gstNumber && data.gstNumber.length > 0; const baseAmount = hasGst ? data.amount / 1.18 : data.amount; const sgst = hasGst ? baseAmount * 0.09 : 0; @@ -51,66 +51,110 @@ export class InvoiceService { const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); const doc = new jsPDF(); - + doc.setFontSize(20); doc.setFont('helvetica', 'bold'); doc.text(data.businessName, 20, 20); - + doc.setFontSize(12); doc.setFont('helvetica', 'normal'); - doc.text(data.address, 20, 30); - - if (hasGst) { - doc.text(`GSTIN: ${data.gstNumber}`, 20, 40); + + const maxWidth = 170; + const lineHeight = 5; + + const addressLines = doc.splitTextToSize(data.address, maxWidth); + + if (addressLines.length <= 2) { + for (let i = 0; i < addressLines.length; i++) { + doc.text(addressLines[i], 20, 30 + (i * lineHeight)); + } + } else { + doc.text(addressLines[0], 20, 30); + + let secondLine = addressLines[1]; + if (secondLine.length > 3) { + secondLine = secondLine.substring(0, secondLine.length - 3) + '...'; + } else { + secondLine += '...'; + } + doc.text(secondLine, 20, 35); } - + + const gstYPosition = 40; + + if (hasGst) { + doc.text(`GSTIN: ${data.gstNumber}`, 20, gstYPosition); + } + doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('RECEIPT', 190, 20, { align: 'right' }); - + doc.setFontSize(12); doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); - + doc.line(20, 45, 190, 45); - + doc.setFontSize(12); + const receiptToBoxX = 15; + const receiptToBoxY = 55; + const receiptToBoxWidth = 175; + const receiptToBoxHeight = 45; + + doc.setDrawColor(0, 0, 0); + doc.setLineWidth(0.5); + doc.rect(receiptToBoxX, receiptToBoxY, receiptToBoxWidth, receiptToBoxHeight); doc.setFont('helvetica', 'bold'); doc.text('Receipt To:', 20, 60); - + doc.setFont('helvetica', 'normal'); doc.text(data.customerName, 20, 70); doc.text(`Phone: ${data.phoneNumber}`, 20, 80); doc.text(`Email: ${data.email}`, 20, 90); - - autoTable(doc,{ + + autoTable(doc, { startY: 110, head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], body: [ ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], - headStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: 'bold' }, - styles: { halign: 'center' }, + headStyles: { + fillColor: [220, 220, 220], + textColor: [0, 0, 0], + fontStyle: 'bold', + lineWidth: 0.5, + lineColor: [0, 0, 0] + }, + styles: { + halign: 'center', + lineWidth: 0.5, + lineColor: [0, 0, 0] + }, columnStyles: { 0: { halign: 'center', cellWidth: 20 }, 1: { halign: 'left' }, 2: { halign: 'center', cellWidth: 40 }, 3: { halign: 'right', cellWidth: 40 } - } + }, + theme: 'grid', + tableLineWidth: 0.5, + tableLineColor: [0, 0, 0], + }); - + const finalY = (doc as any).lastAutoTable.finalY + 20; - + if (hasGst) { doc.text('Taxable Amount:', 150, finalY, { align: 'right' }); doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); - + doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' }); doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' }); - + doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); - + doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' }); @@ -119,27 +163,38 @@ export class InvoiceService { doc.text('Total Amount:', 150, finalY, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); } - + const paymentY = hasGst ? finalY + 50 : finalY + 20; - + + doc.line(20, 45, 190, 45); + + const boxX = 15; + const boxY = paymentY - 5; + const boxWidth = 175; + const boxHeight = 45; + + doc.setDrawColor(0, 0, 0); + doc.setLineWidth(0.5); + doc.rect(boxX, boxY, boxWidth, boxHeight); + doc.setFont('helvetica', 'bold'); doc.text('Payment Information:', 20, paymentY); - + doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10); doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20); doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); - + doc.setFontSize(12); doc.setFont('helvetica', 'italic'); doc.text('Thank you for your business!', 105, 270, { align: 'center' }); - + doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' }); - + fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer'))); - + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { @@ -148,9 +203,9 @@ export class InvoiceService { contentType: 'application/pdf', }, }); - + fs.unlinkSync(tempFilePath); - + return invoicePath; } catch (error: any) { logger.error('Error generating invoice:', error); @@ -162,15 +217,15 @@ export class InvoiceService { try { const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); - - const expirationMs = 7 * 24 * 60 * 60 * 1000; - + + const expirationMs = 7 * 24 * 60 * 60 * 1000; + const [signedUrl] = await file.getSignedUrl({ action: 'read', expires: Date.now() + expirationMs, responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`, }); - + return signedUrl; } catch (error: any) { logger.error('Error getting invoice download URL:', error); @@ -183,37 +238,37 @@ export class InvoiceService { const membershipPaymentsRef = admin.firestore() .collection('membership_payments') .doc(membershipId); - + const docSnapshot = await membershipPaymentsRef.get(); - + if (!docSnapshot.exists) { logger.error(`No membership payments found for membershipId: ${membershipId}`); return false; } - + const data = docSnapshot.data(); const paymentsData = data?.payments || []; - + let found = false; for (let i = 0; i < paymentsData.length; i++) { - if (paymentsData[i].referenceNumber === paymentId || - paymentsData[i].transactionId === paymentId) { + if (paymentsData[i].referenceNumber === paymentId || + paymentsData[i].transactionId === paymentId) { paymentsData[i].invoicePath = invoicePath; found = true; break; } } - + if (!found) { logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`); return false; } - + await membershipPaymentsRef.update({ 'payments': paymentsData, 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), }); - + logger.info(`Successfully updated invoice path for payment: ${paymentId}`); return true; } catch (error: any) { @@ -225,11 +280,11 @@ export class InvoiceService { async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { try { const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - - const formattedDate = emailOptions.additionalData?.paymentDate + + const formattedDate = emailOptions.additionalData?.paymentDate ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') : new Date().toLocaleDateString('en-GB'); - + const emailHtml = emailOptions.customHtml || ` @@ -251,7 +306,7 @@ export class InvoiceService { `; - + await sendEmailWithAttachmentUtil( emailOptions.recipientEmail, emailOptions.subject || 'Your Fitlien Membership Invoice', @@ -259,7 +314,7 @@ export class InvoiceService { downloadUrl, `Invoice_${path.basename(invoicePath)}` ); - + logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`); return true; } catch (error: any) { @@ -269,7 +324,7 @@ export class InvoiceService { } async processInvoice( - membershipId: string, + membershipId: string, paymentId: string, invoiceData: InvoiceData, emailOptions?: EmailOptions @@ -282,9 +337,9 @@ export class InvoiceService { }> { try { const invoicePath = await this.generateInvoice(invoiceData); - + const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); - + if (!updateSuccess) { return { success: false, @@ -293,14 +348,14 @@ export class InvoiceService { error: 'Failed to update payment with invoice path' }; } - + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - + let emailSent = false; if (emailOptions && emailOptions.recipientEmail) { emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); } - + return { success: true, invoicePath, diff --git a/functions/src/payments/phonepe/paymentData.ts b/functions/src/payments/phonepe/paymentData.ts index b6eca48..13d7536 100644 --- a/functions/src/payments/phonepe/paymentData.ts +++ b/functions/src/payments/phonepe/paymentData.ts @@ -3,7 +3,6 @@ import { getAdmin, getLogger } from "../../shared/config"; const admin = getAdmin(); const logger = getLogger(); -// Define an interface for the payment data to avoid type errors interface PaymentData { id: string; date: string; @@ -14,7 +13,7 @@ interface PaymentData { discount: any; transactionId: string; createdAt: Date; - invoicePath?: string; // Make this optional + invoicePath?: string; } export async function updatePaymentDataAfterSuccess( @@ -24,7 +23,6 @@ export async function updatePaymentDataAfterSuccess( invoicePath?: string ): Promise { try { - // Get the payment order from Firestore const orderQuery = await admin.firestore() .collection('payment_orders') .where('merchantOrderId', '==', merchantOrderId) @@ -39,19 +37,17 @@ export async function updatePaymentDataAfterSuccess( const orderDoc = orderQuery.docs[0]; const orderData = orderDoc.data(); - // Extract membership ID from metaInfo const membershipId = orderData.metaInfo?.membershipId; if (!membershipId) { logger.error(`No membershipId found in metaInfo for order: ${merchantOrderId}`); return false; } - const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); // DD-MM-YYYY format + const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); const dateTimestamp = admin.firestore.Timestamp.now(); - // Create payment data object with proper typing const paymentData: PaymentData = { - id: admin.firestore().collection('_').doc().id, // Generate a UUID + id: admin.firestore().collection('_').doc().id, date: isoDate, dateTimestamp: dateTimestamp, amount: orderData.amount, @@ -62,19 +58,16 @@ export async function updatePaymentDataAfterSuccess( createdAt: new Date() }; - // Add invoice path if provided if (invoicePath) { paymentData.invoicePath = invoicePath; } - // Get reference to membership payments document const membershipPaymentsRef = admin.firestore() .collection('membership_payments') .doc(membershipId); const docSnapshot = await membershipPaymentsRef.get(); - // Update or create the membership payments document if (docSnapshot.exists) { await membershipPaymentsRef.update({ 'payments': admin.firestore.FieldValue.arrayUnion(paymentData), @@ -92,7 +85,6 @@ export async function updatePaymentDataAfterSuccess( }); } - // Update membership status await updateMembershipStatus(membershipId, orderData.userId); logger.info(`Successfully updated payment data for membership: ${membershipId}`); @@ -118,7 +110,7 @@ async function updateMembershipStatus(membershipId: string, userId: string): Pro .collection('memberships') .doc(membershipId) .update({ - 'status': 'ACTIVE', // Assuming this matches your InvitationStatus.active + 'status': 'ACTIVE', 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), }); From 916293e8b1192851cda1f594be0bb4ff72c22d8d Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 07:15:32 +0000 Subject: [PATCH 71/81] phonepe (#44) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/44 --- .../phonepe/invoice/invoiceService.ts | 26 +++++++++---------- functions/src/payments/phonepe/webhook.ts | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index bcc78a4..741b900 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -54,7 +54,7 @@ export class InvoiceService { doc.setFontSize(20); doc.setFont('helvetica', 'bold'); - doc.text(data.businessName, 20, 20); + doc.text(data.businessName, 13, 20); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); @@ -66,10 +66,10 @@ export class InvoiceService { if (addressLines.length <= 2) { for (let i = 0; i < addressLines.length; i++) { - doc.text(addressLines[i], 20, 30 + (i * lineHeight)); + doc.text(addressLines[i], 13, 30 + (i * lineHeight)); } } else { - doc.text(addressLines[0], 20, 30); + doc.text(addressLines[0], 13, 30); let secondLine = addressLines[1]; if (secondLine.length > 3) { @@ -77,7 +77,7 @@ export class InvoiceService { } else { secondLine += '...'; } - doc.text(secondLine, 20, 35); + doc.text(secondLine, 13, 35); } const gstYPosition = 40; @@ -97,10 +97,10 @@ export class InvoiceService { doc.line(20, 45, 190, 45); doc.setFontSize(12); - const receiptToBoxX = 15; + const receiptToBoxX = 13; const receiptToBoxY = 55; - const receiptToBoxWidth = 175; - const receiptToBoxHeight = 45; + const receiptToBoxWidth = 140; + const receiptToBoxHeight = 40; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -166,12 +166,12 @@ export class InvoiceService { const paymentY = hasGst ? finalY + 50 : finalY + 20; - doc.line(20, 45, 190, 45); + doc.line(13, 45, 190, 45); - const boxX = 15; + const boxX = 13; const boxY = paymentY - 5; - const boxWidth = 175; - const boxHeight = 45; + const boxWidth = 140; + const boxHeight = 40; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -181,8 +181,8 @@ export class InvoiceService { doc.text('Payment Information:', 20, paymentY); doc.setFont('helvetica', 'normal'); - doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10); - doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20); + doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); + doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); doc.setFontSize(12); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 42aa29f..9c1f6cc 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -154,7 +154,7 @@ export const phonePeWebhook = onRequest({ logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() - .collection('client_profiles') + .collection('users') .doc(userId) .get(); if (userDoc.exists) { @@ -245,7 +245,7 @@ export const phonePeWebhook = onRequest({ customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', email: membershipData?.fields?.['email'] || '', - planName: orderData.metaInfo?.planName || subscriptionName || paymentType, + planName: orderData.metaInfo?.planName || subscriptionName, amount: orderData.amount, transactionId: payload.orderId, paymentDate: new Date(), From e2deb441e9b959b88d33d22c7be92d560814cbe9 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 07:37:20 +0000 Subject: [PATCH 72/81] phonepe (#45) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/45 --- functions/src/payments/phonepe/webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9c1f6cc..85a311d 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -158,7 +158,7 @@ export const phonePeWebhook = onRequest({ .doc(userId) .get(); if (userDoc.exists) { - logger.info(`User data retrieved successfully for userId: ${userId}`); + logger.info(`User data retrieved successfully for userId(Client): ${userId}`); logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); From fab7e618d443cc7bf85098968c2100b983f0b58b Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 07:54:53 +0000 Subject: [PATCH 73/81] phonepe (#46) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/46 --- functions/src/payments/phonepe/webhook.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 85a311d..f12eb58 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -150,15 +150,15 @@ export const phonePeWebhook = onRequest({ logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`); const membershipData = membershipDoc.data(); - const userId = membershipData?.userId; + const uid = membershipData?.userId; - logger.info(`Fetching user data for userId: ${userId}`); + logger.info(`Fetching user data for uid(Client): ${uid}`); const userDoc = await admin.firestore() - .collection('users') - .doc(userId) + .collection('client_profiles') + .doc(uid) .get(); if (userDoc.exists) { - logger.info(`User data retrieved successfully for userId(Client): ${userId}`); + logger.info(`User data retrieved successfully for uid(Client): ${uid}`); logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); From a14631553681b21b3a40f555dad166cc2f723d9e Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 08:14:11 +0000 Subject: [PATCH 74/81] phonepe (#47) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/47 --- .../phonepe/invoice/invoiceService.ts | 21 ++++++++++--------- functions/src/payments/phonepe/webhook.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 741b900..df7afcf 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -99,19 +99,19 @@ export class InvoiceService { doc.setFontSize(12); const receiptToBoxX = 13; const receiptToBoxY = 55; - const receiptToBoxWidth = 140; + const receiptToBoxWidth = 100; const receiptToBoxHeight = 40; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); doc.rect(receiptToBoxX, receiptToBoxY, receiptToBoxWidth, receiptToBoxHeight); doc.setFont('helvetica', 'bold'); - doc.text('Receipt To:', 20, 60); + doc.text('Receipt To:', 18, 60); doc.setFont('helvetica', 'normal'); - doc.text(data.customerName, 20, 70); - doc.text(`Phone: ${data.phoneNumber}`, 20, 80); - doc.text(`Email: ${data.email}`, 20, 90); + doc.text(data.customerName, 18, 70); + doc.text(`Phone: ${data.phoneNumber}`, 18, 80); + doc.text(`Email: ${data.email}`, 18, 90); autoTable(doc, { startY: 110, @@ -155,10 +155,13 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); + doc.line(120, finalY + 25, 190, finalY + 25); + doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' }); } else { + doc.line(120, finalY - 5, 190, finalY - 5); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); @@ -166,11 +169,9 @@ export class InvoiceService { const paymentY = hasGst ? finalY + 50 : finalY + 20; - doc.line(13, 45, 190, 45); - const boxX = 13; const boxY = paymentY - 5; - const boxWidth = 140; + const boxWidth = 100; const boxHeight = 40; doc.setDrawColor(0, 0, 0); @@ -178,12 +179,12 @@ export class InvoiceService { doc.rect(boxX, boxY, boxWidth, boxHeight); doc.setFont('helvetica', 'bold'); - doc.text('Payment Information:', 20, paymentY); + doc.text('Payment Information:', 18, paymentY); doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); - doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); + doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 30); doc.setFontSize(12); doc.setFont('helvetica', 'italic'); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index f12eb58..2ba5136 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -197,7 +197,7 @@ export const phonePeWebhook = onRequest({ const gymData = gymDoc.data(); gymName = gymData?.name || 'Fitlien'; gymAddress = gymData?.address || ''; - subscriptionName = gymData?.subscriptions?.name || ''; + subscriptionName = membershipData?.subscription?.normalizedName || ''; if (gymData?.userId) { const gymOwnerDoc = await admin.firestore() From 33b01e5e5b1f20ebac9b3d35eee38a51445ed9bc Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 09:00:10 +0000 Subject: [PATCH 75/81] phonepe (#48) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/48 --- .../phonepe/invoice/invoiceService.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index df7afcf..fcf9cec 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -54,7 +54,7 @@ export class InvoiceService { doc.setFontSize(20); doc.setFont('helvetica', 'bold'); - doc.text(data.businessName, 13, 20); + doc.text(data.businessName, 15, 20); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); @@ -66,10 +66,10 @@ export class InvoiceService { if (addressLines.length <= 2) { for (let i = 0; i < addressLines.length; i++) { - doc.text(addressLines[i], 13, 30 + (i * lineHeight)); + doc.text(addressLines[i], 15, 30 + (i * lineHeight)); } } else { - doc.text(addressLines[0], 13, 30); + doc.text(addressLines[0], 15, 30); let secondLine = addressLines[1]; if (secondLine.length > 3) { @@ -77,7 +77,7 @@ export class InvoiceService { } else { secondLine += '...'; } - doc.text(secondLine, 13, 35); + doc.text(secondLine, 15, 35); } const gstYPosition = 40; @@ -88,17 +88,17 @@ export class InvoiceService { doc.setFontSize(24); doc.setFont('helvetica', 'bold'); - doc.text('RECEIPT', 190, 20, { align: 'right' }); + doc.text('RECEIPT', 195, 20, { align: 'right' }); doc.setFontSize(12); - doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); - doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); + doc.text(`Receipt #: ${data.invoiceNumber}`, 195, 30, { align: 'right' }); + doc.text(`Date: ${formattedDate}`, 195, 40, { align: 'right' }); - doc.line(20, 45, 190, 45); + doc.line(15, 45, 195, 45); doc.setFontSize(12); - const receiptToBoxX = 13; - const receiptToBoxY = 55; + const receiptToBoxX = 15; + const receiptToBoxY = 50; const receiptToBoxWidth = 100; const receiptToBoxHeight = 40; @@ -110,12 +110,20 @@ export class InvoiceService { doc.setFont('helvetica', 'normal'); doc.text(data.customerName, 18, 70); - doc.text(`Phone: ${data.phoneNumber}`, 18, 80); - doc.text(`Email: ${data.email}`, 18, 90); + doc.text(`Phone: ${data.phoneNumber}`, 18, 75); + doc.text(`Email: ${data.email}`, 18, 80); autoTable(doc, { startY: 110, - head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], + margin: {left: 15, right: 15}, + head: [ + [ + {content: 'No.', styles: {halign: 'center'}}, + {content: 'Description', styles: {halign: 'left'}}, + {content: 'HSN/SAC', styles: {halign: 'center'}}, + {content: 'Amount (INR)', styles: {halign: 'right'}} + ] + ], body: [ ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], @@ -147,30 +155,30 @@ export class InvoiceService { if (hasGst) { doc.text('Taxable Amount:', 150, finalY, { align: 'right' }); - doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 195, finalY, { align: 'right' }); doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' }); - doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' }); + doc.text(`${sgst.toFixed(2)} INR`, 195, finalY + 10, { align: 'right' }); doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); - doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); + doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(120, finalY + 25, 190, finalY + 25); + doc.line(120, finalY + 25, 195, finalY + 25); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); - doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY + 30, { align: 'right' }); } else { - doc.line(120, finalY - 5, 190, finalY - 5); + doc.line(120, finalY - 5, 195, finalY - 5); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY, { align: 'right' }); - doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY, { align: 'right' }); } const paymentY = hasGst ? finalY + 50 : finalY + 20; - const boxX = 13; - const boxY = paymentY - 5; + const boxX = 15; + const boxY = paymentY - 10; const boxWidth = 100; const boxHeight = 40; @@ -183,8 +191,8 @@ export class InvoiceService { doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); - doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); - doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 30); + doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 15); + doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 20); doc.setFontSize(12); doc.setFont('helvetica', 'italic'); From fbacda6806225f22f985bf12774427ca5e868e4c Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 09:11:01 +0000 Subject: [PATCH 76/81] phonepe (#49) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/49 --- functions/src/payments/phonepe/invoice/invoiceService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index fcf9cec..60b88a0 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -100,7 +100,7 @@ export class InvoiceService { const receiptToBoxX = 15; const receiptToBoxY = 50; const receiptToBoxWidth = 100; - const receiptToBoxHeight = 40; + const receiptToBoxHeight = 36; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -163,7 +163,7 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(120, finalY + 25, 195, finalY + 25); + doc.line(15, finalY + 25, 15, finalY + 25); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); @@ -180,7 +180,7 @@ export class InvoiceService { const boxX = 15; const boxY = paymentY - 10; const boxWidth = 100; - const boxHeight = 40; + const boxHeight = 36; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); From e12cbf4148b7cc3eba69bf965a9b1d456f8f2227 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Fri, 23 May 2025 09:31:52 +0000 Subject: [PATCH 77/81] phonepe (#50) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/50 --- functions/src/payments/phonepe/invoice/invoiceService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 60b88a0..df4fbe4 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -94,6 +94,7 @@ export class InvoiceService { doc.text(`Receipt #: ${data.invoiceNumber}`, 195, 30, { align: 'right' }); doc.text(`Date: ${formattedDate}`, 195, 40, { align: 'right' }); + doc.setLineWidth(0.5); doc.line(15, 45, 195, 45); doc.setFontSize(12); @@ -163,13 +164,15 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(15, finalY + 25, 15, finalY + 25); + doc.setLineWidth(0.5); + doc.line(15, finalY + 25, 195, finalY + 25); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY + 30, { align: 'right' }); } else { - doc.line(120, finalY - 5, 195, finalY - 5); + doc.setLineWidth(0.5); + doc.line(15, finalY - 5, 195, finalY - 5); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY, { align: 'right' }); From a89168bb74dda90b4e7543ff88431a22acb6cd49 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 26 May 2025 20:01:19 +0530 Subject: [PATCH 78/81] Removed unused function --- .gitea/workflows/deploy-dev.yaml | 3 - functions/src/email/index.ts | 2 - functions/src/email/sendEmail.ts | 40 --------- .../src/email/sendEmailWithAttachment.ts | 81 ------------------- 4 files changed, 126 deletions(-) delete mode 100644 functions/src/email/sendEmail.ts delete mode 100644 functions/src/email/sendEmailWithAttachment.ts diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 7862777..413dd79 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -27,9 +27,6 @@ jobs: - name: Replace variables in .env run: | - sed -i "s/#{MAILGUN_API_KEY}#/${{ secrets.MAILGUN_API_KEY }}/" functions/.env - sed -i "s/#{MAILGUN_SERVER}#/${{ secrets.MAILGUN_SERVER }}/" functions/.env - sed -i "s/#{MAILGUN_FROM_ADDRESS}#/${{ secrets.MAILGUN_FROM_ADDRESS }}/" functions/.env sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env diff --git a/functions/src/email/index.ts b/functions/src/email/index.ts index 8a5ad52..53ac110 100644 --- a/functions/src/email/index.ts +++ b/functions/src/email/index.ts @@ -1,3 +1 @@ -export { sendEmailMessage } from './sendEmail'; -export { sendEmailWithAttachment } from './sendEmailWithAttachment'; export { sendEmailSES } from './sendEmailSES'; diff --git a/functions/src/email/sendEmail.ts b/functions/src/email/sendEmail.ts deleted file mode 100644 index 4200b76..0000000 --- a/functions/src/email/sendEmail.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import { getCorsHandler } from "../shared/middleware"; -import { getLogger } from "../shared/config"; -import formData from 'form-data'; -import Mailgun from 'mailgun.js'; -const { convert } = require('html-to-text'); - -const mailgun = new Mailgun(formData); -const logger = getLogger(); -const corsHandler = getCorsHandler(); -export const sendEmailMessage = onRequest({ - region: '#{SERVICES_RGN}#' -}, (request: Request, response) => { - return corsHandler(request, response, async () => { - const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); - - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const options = { - wordwrap: 130, - }; - - const textMessage = convert(message, options); - mailGunClient.messages.create(process.env.MAILGUN_SERVER!, { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message - }).then((res: any) => { - logger.info(res); - response.send(res); - }).catch((err: any) => { - logger.error(err); - response.send(err); - }); - }); -}); \ No newline at end of file diff --git a/functions/src/email/sendEmailWithAttachment.ts b/functions/src/email/sendEmailWithAttachment.ts deleted file mode 100644 index 7c1bc74..0000000 --- a/functions/src/email/sendEmailWithAttachment.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import * as os from 'os'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as https from 'https'; -import { getCorsHandler } from "../shared/middleware"; -import { getLogger } from "../shared/config"; -import formData from 'form-data'; -import Mailgun from 'mailgun.js'; -const { convert } = require('html-to-text'); - -const mailgun = new Mailgun(formData); -const logger = getLogger(); -const corsHandler = getCorsHandler(); -export const sendEmailWithAttachment = onRequest({ - region: '#{SERVICES_RGN}#' -}, async (request: Request, response) => { - return corsHandler(request, response, async () => { - try { - const { toAddress, subject, message, fileUrl, fileName } = request.body; - - if (!toAddress || !subject || !message || !fileUrl) { - response.status(400).json({ - error: 'Missing required fields (toAddress, subject, message, fileUrl)' - }); - return; - } - - const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(tempFilePath); - https.get(fileUrl, (res) => { - res.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', (err) => { - fs.unlink(tempFilePath, () => { }); - reject(err); - }); - }); - - try { - const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); - - const options = { - wordwrap: 130, - }; - const textMessage = convert(message, options); - const fileBuffer = fs.readFileSync(tempFilePath); - const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); - - const data = { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message, - attachment: { - data: fileBuffer, - filename: attachmentFilename, - contentType: 'application/pdf', - } - }; - - const result = await client.messages.create(process.env.MAILGUN_SERVER!, data); - fs.unlinkSync(tempFilePath); - logger.info('Email with attachment from URL sent successfully'); - response.json({ success: true, result }); - - } catch (e) { - console.error(`Error while sending E-mail. Error: ${e}`); - } - } catch (error) { - logger.error('Error sending email with attachment from URL:', error); - response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) }); - } - }); -}); \ No newline at end of file From be7470f1e42bbd7fca9981efde79112d90fecda6 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 26 May 2025 20:08:07 +0530 Subject: [PATCH 79/81] Update deploy-dev.yaml --- .gitea/workflows/deploy-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml index 413dd79..f0f0a82 100644 --- a/.gitea/workflows/deploy-dev.yaml +++ b/.gitea/workflows/deploy-dev.yaml @@ -47,7 +47,7 @@ jobs: - name: "Replace #{SERVICES_RGN}# in all .ts files" run: | find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + - cat functions/src/email/sendEmail.ts + - name: Build run: | npm install -g typescript From 3f3bf53e942fd7e4f1855dbf41f359e095dc6e89 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 26 May 2025 20:12:00 +0530 Subject: [PATCH 80/81] Update index.ts --- functions/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 660ddc7..432a73c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -10,7 +10,7 @@ setGlobalOptions({ }); export * from './shared/config'; -export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email'; +export { sendEmailSES } from './email'; export { sendSMSMessage } from './sms'; export { accessFile } from './storage'; export { processNotificationOnCreate } from './notifications'; From 59c4f88ff031f9ed0c0cc8b0f927a39e1111b974 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Mon, 26 May 2025 23:37:47 +0530 Subject: [PATCH 81/81] Create deploy-qa.yaml --- .gitea/workflows/deploy-qa.yaml | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .gitea/workflows/deploy-qa.yaml diff --git a/.gitea/workflows/deploy-qa.yaml b/.gitea/workflows/deploy-qa.yaml new file mode 100644 index 0000000..9607ecf --- /dev/null +++ b/.gitea/workflows/deploy-qa.yaml @@ -0,0 +1,64 @@ +name: Deploy FitLien services to Dev + +on: + push: + branches: + - qa +jobs: + deploy: + name: Deploy to QA + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 22 + + - name: Clean install + run: npm clean-install + + - name: Copy .env.example to .env + run: cp functions/.env.example functions/.env + + - name: Replace variables in .env + run: | + sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env + sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env + sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env + sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env + sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env + sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env + sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env + sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env + sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env + sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env + sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env + sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env + sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env + + cat functions/.env + - name: "Replace #{SERVICES_RGN}# in all .ts files" + run: | + find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} + + + - name: Build + run: | + npm install -g typescript + cd functions + npm install + npx tsc + cd .. + ls -la + + - name: Deploy + run: | + curl -sL firebase.tools | upgrade=true bash + firebase use --token ${{ secrets.FIREBASE_TOKEN }} qa + firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive