diff --git a/firestore.indexes.json b/firestore.indexes.json index 8d0c63f..5609cde 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -166,6 +166,52 @@ } ] }, + { + "collectionGroup": "workout_logs", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "user_id", + "order": "ASCENDING" + }, + { + "fieldPath": "date", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "workout_logs", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "user_id", + "order": "ASCENDING" + }, + { + "fieldPath": "date", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "workout_logs", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "user_id", + "order": "ASCENDING" + }, + { + "fieldPath": "start_time", + "order": "ASCENDING" + }, + { + "fieldPath": "date", + "order": "ASCENDING" + } + ] + }, { "collectionGroup": "terms_and_conditions", "queryScope": "COLLECTION", diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts index 5efc40a..d461557 100644 --- a/functions/src/email/sendEmailSES.ts +++ b/functions/src/email/sendEmailSES.ts @@ -37,7 +37,7 @@ const stripHtml = (html: string): string => { async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ - region: 'ap-south-1', + region: '#{AWS_REGION}#', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' @@ -63,7 +63,7 @@ async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ - region: '#{SERVICES_RGN}#', + region: 'ap-south-1', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' diff --git a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts index df433f4..e2a5f36 100644 --- a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts +++ b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts @@ -1,62 +1,62 @@ -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"; +// 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(); +// 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; - } +// 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]; +// const idToken = authHeader.split('Bearer ')[1]; - try { - await admin.auth().verifyIdToken(idToken); +// try { +// await admin.auth().verifyIdToken(idToken); - const { invoicePath } = request.query; +// const { invoicePath } = request.query; - if (!invoicePath) { - response.status(400).json({ - success: false, - error: 'Missing invoice path' - }); - return; - } +// if (!invoicePath) { +// response.status(400).json({ +// success: false, +// error: 'Missing invoice path' +// }); +// return; +// } - const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); +// const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); - response.json({ - success: true, - downloadUrl - }); +// 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 - }); - } - }); -}); +// } 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 index daa2183..393c6c1 100644 --- a/functions/src/payments/phonepe/invoice/index.ts +++ b/functions/src/payments/phonepe/invoice/index.ts @@ -1,13 +1,13 @@ -import { getInvoiceUrl } from './getInvoiceUrl'; +// import { getInvoiceUrl } from './getInvoiceUrl'; import { InvoiceService } from './invoiceService'; -import { processInvoice } from './processInvoice'; -import { sendInvoiceEmail } from './sendInvoiceEmail'; +// import { processInvoice } from './processInvoice'; +// import { sendInvoiceEmail } from './sendInvoiceEmail'; import { directGenerateInvoice } from './directInvoice'; export { - getInvoiceUrl, + // getInvoiceUrl, InvoiceService, - processInvoice, - sendInvoiceEmail, + // processInvoice, + // sendInvoiceEmail, directGenerateInvoice, }; diff --git a/functions/src/payments/phonepe/invoice/processInvoice.ts b/functions/src/payments/phonepe/invoice/processInvoice.ts index 1164d89..490b0a4 100644 --- a/functions/src/payments/phonepe/invoice/processInvoice.ts +++ b/functions/src/payments/phonepe/invoice/processInvoice.ts @@ -1,83 +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"; +// 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(); +// 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; - } +// 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]; +// const idToken = authHeader.split('Bearer ')[1]; - try { - await admin.auth().verifyIdToken(idToken); +// try { +// await admin.auth().verifyIdToken(idToken); - const { - membershipId, - paymentId, - invoiceData, - emailOptions - } = request.body; +// const { +// membershipId, +// paymentId, +// invoiceData, +// emailOptions +// } = request.body; - if (!membershipId || !paymentId || !invoiceData) { - response.status(400).json({ - success: false, - error: 'Missing required fields' - }); - return; - } +// if (!membershipId || !paymentId || !invoiceData) { +// response.status(400).json({ +// success: false, +// error: 'Missing required fields' +// }); +// return; +// } - const result = await invoiceService.processInvoice( - membershipId, - paymentId, - invoiceData, - emailOptions - ); +// 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; - } +// 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 - }); +// 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 - }); - } - }); -}); +// } 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 index 2af677a..bf51edf 100644 --- a/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts +++ b/functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts @@ -1,91 +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"; +// 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(); +// 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; - } +// 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]; +// const idToken = authHeader.split('Bearer ')[1]; - try { - await admin.auth().verifyIdToken(idToken); +// try { +// await admin.auth().verifyIdToken(idToken); - const { - invoicePath, - recipientEmail, - recipientName, - subject, - customHtml, - gymName, - planName, - amount, - transactionId, - paymentDate, - paymentMethod - } = request.body; +// 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; - } +// 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 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); +// 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 - }); +// 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 - }); - } - }); -}); +// } 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/webhook.ts b/functions/src/payments/phonepe/webhook.ts index cfb462a..2e35a3e 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -120,7 +120,7 @@ export const phonePeWebhook = onRequest({ const orderData = orderDoc.data(); const membershipId = orderData.metaInfo?.membershipId; const bookingId = orderData.metaInfo?.bookingId; - const serviceId = orderData.metaInfo?.serviceId; + const paymentId = orderData.metaInfo?.paymentId; if (bookingId) { await processDayPassBooking(payload, orderData, bookingId); @@ -139,10 +139,10 @@ export const phonePeWebhook = onRequest({ if (paymentUpdateSuccess) { await processMembershipPayment(payload, orderData, membershipId); } - } else if (serviceId) { - await processServicePayment(payload, orderData, serviceId); + } else if (paymentId) { + await processServicePayment(payload, orderData, paymentId); } else { - logger.error(`No membershipId, bookingId, or serviceId found in metaInfo for order: ${payload.merchantOrderId}`); + logger.error(`No membershipId, bookingId, or paymentId found in metaInfo for order: ${payload.merchantOrderId}`); } logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`); @@ -639,6 +639,18 @@ async function processServicePayment(payload: any, orderData: any, paymentId: st return; } + const serviceData = serviceDoc.data(); + + if (serviceData?.status === 'ACCEPTED' && serviceData?.paymentDetails?.merchantOrderId) { + logger.warn(`Service payment already processed for serviceId: ${paymentId}, merchantOrderId: ${serviceData.paymentDetails.merchantOrderId}`); + return; + } + + if (serviceData?.invoicePath && serviceData?.invoiceNumber) { + logger.warn(`Invoice already exists for serviceId: ${paymentId}, invoicePath: ${serviceData.invoicePath}`); + return; + } + await serviceRef.update({ status: 'ACCEPTED', paymentDetails: { @@ -651,9 +663,8 @@ async function processServicePayment(payload: any, orderData: any, paymentId: st updatedAt: admin.firestore.FieldValue.serverTimestamp() }); - logger.info(`Updated service booking status to 'CONFIRMED' for serviceId: ${paymentId}`); + logger.info(`Updated service booking status to 'CONFIRMED' for paymentId: ${paymentId}`); - const serviceData = serviceDoc.data(); const gymId = orderData.metaInfo?.gymId || serviceData?.gymId; if (gymId) { @@ -681,7 +692,7 @@ async function processServicePayment(payload: any, orderData: any, paymentId: st } } - const invoiceNumber = `SRV-${payload.merchantOrderId.substring(0, 8)}`; + const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; logger.info(`Generated invoice number for service: ${invoiceNumber}`); const discountPercentage = orderData.metaInfo?.discount || 0; @@ -703,7 +714,7 @@ async function processServicePayment(payload: any, orderData: any, paymentId: st businessName: gymName, address: gymAddress, gstNumber: orderData.metaInfo?.gstNumber, - customerName: orderData.metaInfo?.customerName || serviceData?.customerName || '', + customerName: orderData.metaInfo?.customerName || serviceData?.normalizedName || '', phoneNumber: orderData.metaInfo?.customerPhone || serviceData?.phoneNumber || '', email: orderData.metaInfo?.customerEmail || serviceData?.email || '', planName: orderData.metaInfo?.serviceName || serviceData?.serviceName || 'Service', diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts index 1bb4e06..713061c 100644 --- a/functions/src/utils/emailService.ts +++ b/functions/src/utils/emailService.ts @@ -21,7 +21,7 @@ interface EmailRequest { interface Attachment { filename: string; - content: string | Buffer; // Base64 encoded string or Buffer + content: string | Buffer; contentType?: string; } @@ -32,7 +32,7 @@ const stripHtml = (html: string): string => { async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ - region: 'ap-south-1', + region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' @@ -58,7 +58,7 @@ async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ - region: 'ap-south-1', + region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' @@ -72,26 +72,21 @@ async function sendEmailWithAttachments(data: EmailRequest, recipients: string[] 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) || @@ -109,7 +104,6 @@ async function sendEmailWithAttachments(data: EmailRequest, recipients: string[] rawMessage += contentBuffer.toString('base64') + '\n\n'; } - // Close message rawMessage += `--${boundary}--`; const command = new SendRawEmailCommand({ @@ -140,7 +134,6 @@ export async function sendEmailWithAttachmentUtil( try { logger.info(`Sending email with attachment to: ${toAddress}`); - // Initialize data with basic fields const data: EmailRequest = { to: toAddress, html: message, @@ -151,13 +144,11 @@ export async function sendEmailWithAttachmentUtil( attachments: [] }; - // Handle file URL if provided if (fileUrl && fileName) { logger.info(`Downloading attachment from URL: ${fileUrl}`); try { const fileContent = await downloadFileFromUrl(fileUrl); - // Add the downloaded file as an attachment data.attachments!.push({ filename: fileName, content: fileContent,