Invoice
This commit is contained in:
parent
6d64f1e4d7
commit
e8710074c4
@ -25,7 +25,7 @@
|
|||||||
"port": 5005
|
"port": 5005
|
||||||
},
|
},
|
||||||
"firestore": {
|
"firestore": {
|
||||||
"port": 8085
|
"port": 8086
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"port": 9199
|
"port": 9199
|
||||||
|
|||||||
@ -3,6 +3,6 @@ export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email
|
|||||||
export { sendSMSMessage } from './sms';
|
export { sendSMSMessage } from './sms';
|
||||||
export { accessFile } from './storage';
|
export { accessFile } from './storage';
|
||||||
export { processNotificationOnCreate } from './notifications';
|
export { processNotificationOnCreate } from './notifications';
|
||||||
export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment, checkPhonePePaymentStatus, createPhonePeOrder, phonePeWebhook } from './payments';
|
export * from './payments';
|
||||||
export { getPlaceDetails, getPlacesAutocomplete } from './places';
|
export { getPlaceDetails, getPlacesAutocomplete } from './places';
|
||||||
export { registerClient } from './clientRegistration';
|
export { registerClient } from './clientRegistration';
|
||||||
|
|||||||
@ -3,9 +3,13 @@ import { onRequest } from "firebase-functions/v2/https";
|
|||||||
import { Request} from "firebase-functions/v2/https";
|
import { Request} from "firebase-functions/v2/https";
|
||||||
import { getCorsHandler } from "../../shared/middleware";
|
import { getCorsHandler } from "../../shared/middleware";
|
||||||
import { getAdmin, getLogger } from "../../shared/config";
|
import { getAdmin, getLogger } from "../../shared/config";
|
||||||
|
import { updatePaymentDataAfterSuccess } from "./paymentData";
|
||||||
|
import { InvoiceService } from "./invoice/invoiceService";
|
||||||
|
|
||||||
const admin = getAdmin();
|
const admin = getAdmin();
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
const corsHandler = getCorsHandler();
|
const corsHandler = getCorsHandler();
|
||||||
|
const invoiceService = new InvoiceService();
|
||||||
|
|
||||||
export const checkPhonePePaymentStatus = onRequest({
|
export const checkPhonePePaymentStatus = onRequest({
|
||||||
region: '#{SERVICES_RGN}#'
|
region: '#{SERVICES_RGN}#'
|
||||||
@ -91,12 +95,126 @@ export const checkPhonePePaymentStatus = onRequest({
|
|||||||
}
|
}
|
||||||
|
|
||||||
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',
|
orderStatus: statusResponse.data.state || 'UNKNOWN',
|
||||||
lastChecked: new Date(),
|
lastChecked: new Date(),
|
||||||
statusResponse: statusResponse.data
|
statusResponse: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data));
|
logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data));
|
||||||
|
|
||||||
response.json({
|
response.json({
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
export { createPhonePeOrder } from './createPhonepeOrder';
|
export { createPhonePeOrder } from './createPhonepeOrder';
|
||||||
export { checkPhonePePaymentStatus } from './checkStatus';
|
export { checkPhonePePaymentStatus } from './checkStatus';
|
||||||
export { phonePeWebhook } from './webhook';
|
export { phonePeWebhook } from './webhook';
|
||||||
|
export { updatePaymentDataAfterSuccess } from './paymentData';
|
||||||
|
export * from './invoice';
|
||||||
|
|||||||
106
functions/src/payments/phonepe/invoice/directInvoice.ts
Normal file
106
functions/src/payments/phonepe/invoice/directInvoice.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
63
functions/src/payments/phonepe/invoice/getInvoiceUrl.ts
Normal file
63
functions/src/payments/phonepe/invoice/getInvoiceUrl.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
12
functions/src/payments/phonepe/invoice/index.ts
Normal file
12
functions/src/payments/phonepe/invoice/index.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
327
functions/src/payments/phonepe/invoice/invoiceService.ts
Normal file
327
functions/src/payments/phonepe/invoice/invoiceService.ts
Normal file
@ -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<string> {
|
||||||
|
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<void>((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<string> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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 || `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>Thank you for your payment</h2>
|
||||||
|
<p>Dear ${emailOptions.recipientName || 'Valued Customer'},</p>
|
||||||
|
<p>Thank you for your payment. Your membership has been successfully activated.</p>
|
||||||
|
<p>Please find attached your invoice for the payment.</p>
|
||||||
|
<p>Membership Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Gym: ${emailOptions.additionalData?.gymName || 'Fitlien'}</li>
|
||||||
|
<li>Plan: ${emailOptions.additionalData?.planName || 'Membership'}</li>
|
||||||
|
<li>Amount: ₹${emailOptions.additionalData?.amount || '0'}</li>
|
||||||
|
<li>Transaction ID: ${emailOptions.additionalData?.transactionId || 'N/A'}</li>
|
||||||
|
<li>Date: ${formattedDate}</li>
|
||||||
|
<li>Payment Method: ${emailOptions.additionalData?.paymentMethod || 'Online'}</li>
|
||||||
|
</ul>
|
||||||
|
<p>If you have any questions, please contact us.</p>
|
||||||
|
<p>Regards,<br>Fitlien Team</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
functions/src/payments/phonepe/invoice/processInvoice.ts
Normal file
83
functions/src/payments/phonepe/invoice/processInvoice.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
91
functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts
Normal file
91
functions/src/payments/phonepe/invoice/sendInvoiceEmail.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
130
functions/src/payments/phonepe/paymentData.ts
Normal file
130
functions/src/payments/phonepe/paymentData.ts
Normal file
@ -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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,20 @@ import { onRequest } from "firebase-functions/v2/https";
|
|||||||
import { Request } from "firebase-functions/v2/https";
|
import { Request } from "firebase-functions/v2/https";
|
||||||
import { getAdmin, getLogger } from "../../shared/config";
|
import { getAdmin, getLogger } from "../../shared/config";
|
||||||
import crypto from "crypto";
|
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 admin = getAdmin();
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
|
const invoiceService = new InvoiceService();
|
||||||
|
|
||||||
export const phonePeWebhook = onRequest({
|
export const phonePeWebhook = onRequest({
|
||||||
region: '#{SERVICES_RGN}#'
|
region: '#{SERVICES_RGN}#'
|
||||||
}, async (request: Request, response) => {
|
}, async (request: Request, response) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
logger.info('Received webhook request', {
|
logger.info('Received webhook request', {
|
||||||
headers: request.headers,
|
headers: request.headers,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
@ -61,6 +66,8 @@ export const phonePeWebhook = onRequest({
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
let orderDoc;
|
||||||
|
|
||||||
if (orderQuery.empty) {
|
if (orderQuery.empty) {
|
||||||
const merchantOrderQuery = await admin.firestore()
|
const merchantOrderQuery = await admin.firestore()
|
||||||
.collection('payment_orders')
|
.collection('payment_orders')
|
||||||
@ -77,7 +84,7 @@ export const phonePeWebhook = onRequest({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderDoc = merchantOrderQuery.docs[0];
|
orderDoc = merchantOrderQuery.docs[0];
|
||||||
await orderDoc.ref.update({
|
await orderDoc.ref.update({
|
||||||
orderStatus: payload.state || 'UNKNOWN',
|
orderStatus: payload.state || 'UNKNOWN',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
@ -87,7 +94,7 @@ export const phonePeWebhook = onRequest({
|
|||||||
|
|
||||||
logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`);
|
logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`);
|
||||||
} else {
|
} else {
|
||||||
const orderDoc = orderQuery.docs[0];
|
orderDoc = orderQuery.docs[0];
|
||||||
await orderDoc.ref.update({
|
await orderDoc.ref.update({
|
||||||
orderStatus: payload.state || 'UNKNOWN',
|
orderStatus: payload.state || 'UNKNOWN',
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
@ -98,6 +105,301 @@ export const phonePeWebhook = onRequest({
|
|||||||
logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`);
|
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 = `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}</h2>
|
||||||
|
<p>Dear ${invoiceData.customerName},</p>
|
||||||
|
<p>${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}</p>
|
||||||
|
<p>Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.</p>
|
||||||
|
<p>Membership Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Gym: ${gymName}</li>
|
||||||
|
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Your Personal Trainer'}</li>` : ''}
|
||||||
|
<li>Plan: ${invoiceData.planName}</li>
|
||||||
|
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>You Save: ₹${amountSaved.toFixed(2)}</li>` : ''}
|
||||||
|
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
|
||||||
|
<li>Transaction ID: ${payload.merchantOrderId}</li>
|
||||||
|
<li>Date: ${formattedDate}</li>
|
||||||
|
${isFreeplan ? '<li>Payment Method: Online}</li>' : ''}
|
||||||
|
</ul>
|
||||||
|
<p>If you have any questions, please contact us.</p>
|
||||||
|
<p>Regards,<br>Fitlien Team</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}</h2>
|
||||||
|
<p>Dear Gym Owner,</p>
|
||||||
|
<p>${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.</p>
|
||||||
|
<p>Customer Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Name: ${invoiceData.customerName}</li>
|
||||||
|
<li>Email: ${invoiceData.email}</li>
|
||||||
|
<li>Phone: ${invoiceData.phoneNumber}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Booking Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Type: ${invoiceData.planName}</li>
|
||||||
|
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Personal Trainer'}</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>Amount Saved by Customer: ₹${amountSaved.toFixed(2)}</li>` : ''}
|
||||||
|
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
|
||||||
|
<li>Transaction ID: ${payload.merchantOrderId}</li>
|
||||||
|
<li>Date: ${formattedDate}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Please find the invoice attached.</p>
|
||||||
|
<p>Regards,<br>Fitlien Team</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>New Personal Training Client</h2>
|
||||||
|
<p>Dear ${trainerData.fullName || 'Trainer'},</p>
|
||||||
|
<p>A new client has signed up for personal training with you at ${gymName}.</p>
|
||||||
|
<p>Client Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Name: ${invoiceData.customerName}</li>
|
||||||
|
<li>Email: ${invoiceData.email}</li>
|
||||||
|
<li>Phone: ${invoiceData.phoneNumber}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Booking Details:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Type: Personal Training Membership</li>
|
||||||
|
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
|
||||||
|
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
|
||||||
|
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
|
||||||
|
<li>Transaction ID: ${payload.merchantOrderId}</li>
|
||||||
|
<li>Date: ${formattedDate}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Please find the invoice attached.</p>
|
||||||
|
<p>Regards,<br>Fitlien Team</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 });
|
response.status(200).json({ success: true });
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
70
functions/src/utils/emailService.ts
Normal file
70
functions/src/utils/emailService.ts
Normal file
@ -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<any> {
|
||||||
|
try {
|
||||||
|
const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf');
|
||||||
|
await new Promise<void>((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;
|
||||||
|
}
|
||||||
|
}
|
||||||
206
package-lock.json
generated
206
package-lock.json
generated
@ -6,10 +6,20 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/busboy": "^1.5.4",
|
"@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"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"node_modules/@swc/helpers": {
|
||||||
"@types/long": "^5.0.0"
|
"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": {
|
"node_modules/@types/busboy": {
|
||||||
@ -20,16 +30,6 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.2",
|
"version": "22.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||||
@ -38,6 +38,49 @@
|
|||||||
"undici-types": "~6.20.0"
|
"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": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -49,11 +92,110 @@
|
|||||||
"node": ">=10.16.0"
|
"node": ">=10.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/long": {
|
"node_modules/clone": {
|
||||||
"version": "5.3.1",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
"integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/streamsearch": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -63,10 +205,38 @@
|
|||||||
"node": ">=10.0.0"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.20.0",
|
"version": "6.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/busboy": "^1.5.4",
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user