Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61f0d29f37 | |||
| 209c7c65b0 | |||
| ad31bc8e80 | |||
| f0d167a671 | |||
| e762d0fb79 | |||
| 5ca05c6490 | |||
| 7ec65e4ab1 | |||
| b41431b151 | |||
| 218ff1d02e | |||
| 4f71ca273b | |||
| 4d52d1c7f8 | |||
| aee40521d3 | |||
| a450e93e2b | |||
| 36015d2b83 | |||
| 51fa0825ca | |||
| 3e455fc83a | |||
| 0672a19a60 | |||
| 172fa2edae | |||
| d0c00d8172 | |||
| f93931867c | |||
| 70d76bab2e | |||
| 308cb0fab6 | |||
| 165cd74a17 | |||
| b190a371b6 | |||
| 5543ba5e7a | |||
| 5d6824a6f4 | |||
| abf7a04633 | |||
| 835f478665 | |||
| 5f43a86036 | |||
| 1e09f7a676 | |||
| 5e48f695f8 | |||
| e483b7ad46 | |||
| cd59b9890d | |||
| 939567f7c0 | |||
| 7c494154ba | |||
| 8f5956a825 | |||
| 5ff7b8bb84 | |||
| 7259e67833 | |||
| 81c5241e95 | |||
| 209354ec6b | |||
| 39ea6dcafb | |||
| f6b1545cf6 | |||
| 195262a6de | |||
| d8ae223cce | |||
| c2914b16bb | |||
| b66f1603cc | |||
| d3c9e86c7c | |||
| 4cf5692386 | |||
| 8b308fb9a6 | |||
| 237dd8a263 | |||
| fb23661080 | |||
| cba945c282 | |||
| ef166a209c | |||
| 5d47a78baa | |||
| b594579158 | |||
| 943cff74d5 | |||
| 1fc089a7cb | |||
| 76a75330c8 | |||
| ca08d83f98 | |||
| 0a32e15d05 | |||
| 3223efc392 | |||
| 9c2431fb7b | |||
| 7492cdedc1 | |||
| d8bf928da8 | |||
| 9d51393aa5 | |||
| cc5b6d6987 | |||
| cbe59ee4f1 | |||
| a3241afd45 | |||
| ef2cd80226 | |||
| f19e3b012d | |||
| f2e37e88ed | |||
| a0134466ee | |||
| ecbe9d184b | |||
| 7a796243b0 | |||
| 7db9e479ad | |||
| cf6f4625ad | |||
| 6434f6e3fa | |||
| 2147963523 | |||
| 18569d38d3 | |||
| 5bc3d6dfff | |||
| e8ca80df48 |
@ -19,9 +19,6 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Clean install
|
||||
run: npm clean-install
|
||||
|
||||
- name: Copy .env.example to .env
|
||||
run: cp functions/.env.example functions/.env
|
||||
|
||||
|
||||
@ -19,9 +19,6 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Clean install
|
||||
run: npm clean-install
|
||||
|
||||
- name: Copy .env.example to .env
|
||||
run: cp functions/.env.example functions/.env
|
||||
|
||||
|
||||
@ -19,9 +19,6 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Clean install
|
||||
run: npm clean-install
|
||||
|
||||
- name: Copy .env.example to .env
|
||||
run: cp functions/.env.example functions/.env
|
||||
|
||||
|
||||
@ -57,11 +57,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "memberships",
|
||||
"collectionGroup": "gyms",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gymId",
|
||||
"fieldPath": "userId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
@ -71,79 +71,29 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"collectionGroup": "gyms",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"fieldPath": "isApproved",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
"fieldPath": "createdAt",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"collectionGroup": "memberships",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"fieldPath": "gymId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "trainerId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"fieldPath": "createdAt",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
@ -166,6 +116,66 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "recipientId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
||||
9
functions/package-lock.json
generated
9
functions/package-lock.json
generated
@ -15,7 +15,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"form-data": "^4.0.1",
|
||||
"functions": "file:",
|
||||
"html-to-text": "^9.0.5",
|
||||
@ -4897,9 +4897,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/firebase-functions": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz",
|
||||
"integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.4.0.tgz",
|
||||
"integrity": "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.5",
|
||||
"@types/express": "^4.17.21",
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"firebase-functions": "^6.0.1",
|
||||
"firebase-functions": "^6.4.0",
|
||||
"form-data": "^4.0.1",
|
||||
"functions": "file:",
|
||||
"html-to-text": "^9.0.5",
|
||||
|
||||
@ -207,10 +207,10 @@ function createGetEmployeePunchLogsRequest(username: string, password: string,
|
||||
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap12:Body>
|
||||
<GetEmployeePunchLogs xmlns="http://tempuri.org/">
|
||||
<UserName>cosqclient</UserName>
|
||||
<Password>3bbb58d5</Password>
|
||||
<EmployeeCode>1</EmployeeCode>
|
||||
<AttendanceDate>2025-05-24</AttendanceDate>
|
||||
<UserName>${escapeXml(username)}</UserName>
|
||||
<Password>${escapeXml(password)}</Password>
|
||||
<EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode>
|
||||
<AttendanceDate>${escapeXml(attendanceDate)}</AttendanceDate>
|
||||
</GetEmployeePunchLogs>
|
||||
</soap12:Body>
|
||||
</soap12:Envelope>`;
|
||||
@ -232,9 +232,15 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
|
||||
const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
|
||||
const currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
|
||||
const resultText = currentElement.textContent;
|
||||
if (!resultText || resultText.trim() === '' || resultText.trim() === ';;' || resultText.trim() === ';') {
|
||||
return [];
|
||||
}
|
||||
const punchLogs: Date[] = [];
|
||||
const parts = resultText!.split(';');
|
||||
const parts = resultText.split(';');
|
||||
for (const part of parts) {
|
||||
if (!part || part.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const logDateTime = new Date(part);
|
||||
if (isNaN(logDateTime.getTime())) {
|
||||
@ -245,8 +251,12 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
|
||||
try {
|
||||
const timeParts = part.split(',');
|
||||
for (const timePart of timeParts) {
|
||||
if (!timePart || timePart.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const logDateTime = createDateFromTime(rootDate, timePart);
|
||||
const logDateTime = createDateFromTime(rootDate, timePart.trim());
|
||||
punchLogs.push(logDateTime);
|
||||
} catch {
|
||||
continue;
|
||||
@ -261,6 +271,7 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
|
||||
return sortedLogs;
|
||||
}
|
||||
|
||||
|
||||
async function sendSoapRequest(soapRequest: string, endpoint: string) {
|
||||
try {
|
||||
const headers: any = {
|
||||
|
||||
@ -26,7 +26,7 @@ interface EmailRequest {
|
||||
|
||||
interface Attachment {
|
||||
filename: string;
|
||||
content: string | Buffer; // Base64 encoded string or Buffer
|
||||
content: string | Buffer;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ const stripHtml = (html: string): string => {
|
||||
|
||||
async function sendSimpleEmail(data: EmailRequest, recipients: string[]) {
|
||||
const ses = new SESClient({
|
||||
region: '#{AWS_REGION}#',
|
||||
region: process.env.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: 'ap-south-1',
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || ''
|
||||
|
||||
@ -7,13 +7,13 @@ setGlobalOptions({
|
||||
minInstances: 0,
|
||||
maxInstances: 10,
|
||||
concurrency: 80
|
||||
});
|
||||
});
|
||||
|
||||
export * from './shared/config';
|
||||
export { sendEmailSES } from './email';
|
||||
export { sendSMSMessage } from './sms';
|
||||
export { accessFile } from './storage';
|
||||
export { processNotificationOnCreate } from './notifications';
|
||||
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications';
|
||||
export * from './payments';
|
||||
export { getPlaceDetails, getPlacesAutocomplete } from './places';
|
||||
export { registerClient } from './users';
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export { processNotificationOnCreate } from './processNotification';
|
||||
export { checkExpiredMemberships } from "./membershipStatusNotifications";
|
||||
|
||||
|
||||
1070
functions/src/notifications/membershipStatusNotifications.ts
Normal file
1070
functions/src/notifications/membershipStatusNotifications.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,222 +1,422 @@
|
||||
import { onDocumentCreated } from "firebase-functions/v2/firestore";
|
||||
import { getLogger } from "../shared/config";
|
||||
import { getAdmin } from "../shared/config";
|
||||
import * as admin from 'firebase-admin';
|
||||
import * as admin from "firebase-admin";
|
||||
|
||||
const app = getAdmin();
|
||||
const logger = getLogger();
|
||||
|
||||
interface NotificationData {
|
||||
notificationSent?: boolean;
|
||||
userId?: string;
|
||||
clientId?: string;
|
||||
invitorId?: string;
|
||||
phoneNumber?: string;
|
||||
message?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
gymName?: string;
|
||||
trainerName?: string;
|
||||
membershipId?: string;
|
||||
subscriptionName?: string;
|
||||
name?: string;
|
||||
clientEmail?: string;
|
||||
invitationId?: string;
|
||||
[key: string]: any;
|
||||
senderId?: string;
|
||||
recipientId?: string;
|
||||
type?: string;
|
||||
notificationSent?: boolean;
|
||||
timestamp?: admin.firestore.FieldValue;
|
||||
read?: boolean;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export const processNotificationOnCreate = onDocumentCreated({
|
||||
region: '#{SERVICES_RGN}#',
|
||||
document: 'notifications/{notificationId}'
|
||||
}, async (event) => {
|
||||
export const processNotificationOnCreate = onDocumentCreated(
|
||||
{
|
||||
region: "#{SERVICES_RGN}#",
|
||||
document: "notifications/{notificationId}",
|
||||
},
|
||||
async (event) => {
|
||||
try {
|
||||
const notificationSnapshot = event.data;
|
||||
const notificationId = event.params.notificationId;
|
||||
const notificationSnapshot = event.data;
|
||||
const notificationId = event.params.notificationId;
|
||||
|
||||
if (!notificationSnapshot) {
|
||||
logger.error(`No data found for notification ${notificationId}`);
|
||||
return;
|
||||
}
|
||||
if (!notificationSnapshot) {
|
||||
logger.error(`No data found for notification ${notificationId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = notificationSnapshot.data() as NotificationData;
|
||||
if (notification.notificationSent === true) {
|
||||
logger.info(`Notification ${notificationId} already sent, skipping.`);
|
||||
return;
|
||||
}
|
||||
const notification = notificationSnapshot.data() as NotificationData;
|
||||
|
||||
const { fcmToken } = await getUserAndFCMToken(notification);
|
||||
if (!fcmToken) {
|
||||
logger.error(`FCM token not found for notification ${notificationId}`);
|
||||
await updateNotificationWithError(notificationId, 'FCM token not found for user');
|
||||
return;
|
||||
}
|
||||
if (notification.notificationSent === true) {
|
||||
logger.info(`Notification ${notificationId} already sent, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = prepareNotificationMessage(notification, fcmToken);
|
||||
try {
|
||||
const fcmResponse = await app.messaging().send({
|
||||
...message,
|
||||
token: fcmToken
|
||||
});
|
||||
logger.info(
|
||||
`Processing notification ${notificationId} of type: ${notification.type}`
|
||||
);
|
||||
|
||||
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
|
||||
await markNotificationAsSent(notificationId);
|
||||
const { userId, fcmToken } = await getUserAndFCMToken(notification);
|
||||
if (!fcmToken) {
|
||||
logger.error(
|
||||
`FCM token not found for notification ${notificationId}, user: ${userId}`
|
||||
);
|
||||
await updateNotificationWithError(
|
||||
notificationId,
|
||||
"FCM token not found for user"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification ${notificationId}:`, error);
|
||||
await updateNotificationWithError(notificationId, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
const message = prepareNotificationMessage(notification, fcmToken);
|
||||
try {
|
||||
const fcmResponse = await app.messaging().send(message);
|
||||
|
||||
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
|
||||
await markNotificationAsSent(notificationId);
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification ${notificationId}:`, error);
|
||||
await updateNotificationWithError(
|
||||
notificationId,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing notification:', error);
|
||||
logger.error("Error processing notification:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
async function getUserAndFCMToken(notification: NotificationData): Promise<{ userId: string | null; fcmToken: string | null }> {
|
||||
let userId: string | null = null;
|
||||
let fcmToken: string | null = null;
|
||||
async function getUserAndFCMToken(
|
||||
notification: NotificationData
|
||||
): Promise<{ userId: string | null; fcmToken: string | null }> {
|
||||
let targetUserId: string | null = null;
|
||||
let fcmToken: string | null = null;
|
||||
|
||||
if (notification.userId) {
|
||||
userId = notification.userId;
|
||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||
} else if (notification.clientId) {
|
||||
userId = notification.clientId;
|
||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||
} else if (notification.invitorId) {
|
||||
userId = notification.invitorId;
|
||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||
} else if (notification.phoneNumber) {
|
||||
const userQuery = await app
|
||||
.firestore()
|
||||
.collection('users')
|
||||
.where('phoneNumber', '==', notification.phoneNumber)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (notification.recipientId) {
|
||||
targetUserId = notification.recipientId;
|
||||
logger.info(`Using top-level recipientId: ${targetUserId}`);
|
||||
} else if (notification.data?.phoneNumber) {
|
||||
logger.info(
|
||||
`Looking up user by phone number from data: ${notification.data.phoneNumber}`
|
||||
);
|
||||
const userQuery = await app
|
||||
.firestore()
|
||||
.collection("users")
|
||||
.where("phoneNumber", "==", notification.data.phoneNumber)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!userQuery.empty) {
|
||||
const userDoc = userQuery.docs[0];
|
||||
userId = userDoc.id;
|
||||
fcmToken = userDoc.data()?.fcmToken;
|
||||
}
|
||||
if (!userQuery.empty) {
|
||||
const userDoc = userQuery.docs[0];
|
||||
targetUserId = userDoc.id;
|
||||
fcmToken = userDoc.data()?.fcmToken;
|
||||
logger.info(`Found user by phone: ${targetUserId}`);
|
||||
} else {
|
||||
logger.warn(
|
||||
`No user found with phone number from data: ${notification.data.phoneNumber}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.error("No valid user identifier found in notification or its data");
|
||||
}
|
||||
|
||||
return { userId, fcmToken };
|
||||
if (targetUserId && !fcmToken) {
|
||||
fcmToken = await getFCMTokenFromUserDoc(targetUserId);
|
||||
}
|
||||
|
||||
if (targetUserId && !fcmToken) {
|
||||
logger.warn(`User ${targetUserId} found but no FCM token available`);
|
||||
}
|
||||
|
||||
return { userId: targetUserId, fcmToken };
|
||||
}
|
||||
|
||||
async function getFCMTokenFromUserDoc(userId: string): Promise<string | null> {
|
||||
const userDoc = await app.firestore().collection('users').doc(userId).get();
|
||||
return userDoc.exists ? userDoc.data()?.fcmToken : null;
|
||||
try {
|
||||
const userDoc = await app.firestore().collection("users").doc(userId).get();
|
||||
if (userDoc.exists) {
|
||||
const userData = userDoc.data();
|
||||
const fcmToken = userData?.fcmToken;
|
||||
if (!fcmToken) {
|
||||
logger.warn(`User ${userId} exists but has no FCM token`);
|
||||
}
|
||||
return fcmToken;
|
||||
} else {
|
||||
logger.warn(`User document not found: ${userId}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching user ${userId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareNotificationMessage(notification: NotificationData, fcmToken: string): admin.messaging.Message {
|
||||
let title = 'New Notification';
|
||||
let body = notification.message || 'You have a new notification';
|
||||
let data: Record<string, string> = {
|
||||
type: notification.type || 'general',
|
||||
};
|
||||
function prepareNotificationMessage(
|
||||
notification: NotificationData,
|
||||
fcmToken: string
|
||||
): admin.messaging.TokenMessage {
|
||||
let title = notification.data?.title || "New Notification";
|
||||
let body = notification.data?.message || "You have a new notification";
|
||||
|
||||
switch (notification.type) {
|
||||
case 'day_pass_entry':
|
||||
const isAccepted = notification.status === 'ACCEPTED';
|
||||
title = isAccepted ? 'Day Pass Approved' : 'Day Pass Denied';
|
||||
body = notification.message || (isAccepted ?
|
||||
'Your day pass has been approved' :
|
||||
'Your day pass has been denied');
|
||||
data.gymName = notification.gymName || '';
|
||||
break;
|
||||
let fcmData: Record<string, string> = {
|
||||
type: notification.type || "general",
|
||||
notificationId: "notification_" + Date.now().toString(),
|
||||
};
|
||||
|
||||
case 'trainer_assigned_to_client':
|
||||
title = 'Trainer Assigned';
|
||||
body = notification.message || `${notification.trainerName} has been assigned as your trainer`;
|
||||
data.trainerName = notification.trainerName || '';
|
||||
data.membershipId = notification.membershipId || '';
|
||||
break;
|
||||
if (notification.senderId) fcmData.senderId = notification.senderId;
|
||||
if (notification.recipientId) fcmData.recipientId = notification.recipientId;
|
||||
if (notification.read !== undefined) fcmData.read = String(notification.read);
|
||||
|
||||
case 'client_invitations':
|
||||
if (notification.userId || notification.invitorId) {
|
||||
const isAccept = notification.status === 'ACCEPTED';
|
||||
title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected';
|
||||
body = notification.message || (isAccept ?
|
||||
`The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been accepted` :
|
||||
`The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been rejected`);
|
||||
} else if (notification.phoneNumber) {
|
||||
const invitationStatus = getInvitationStatus(notification.status);
|
||||
title = getInvitationTitle(invitationStatus);
|
||||
body = notification.message || getInvitationBody(invitationStatus, notification.name);
|
||||
data.status = invitationStatus;
|
||||
}
|
||||
data.gymName = notification.gymName || '';
|
||||
data.clientEmail = notification.clientEmail || '';
|
||||
data.clientName = notification.name || '';
|
||||
data.invitationId = notification.invitationId || '';
|
||||
data.subscriptionName = notification.subscriptionName || '';
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.info(`Using default handling for notification type: ${notification.type}`);
|
||||
break;
|
||||
if (notification.data) {
|
||||
for (const key in notification.data) {
|
||||
if (Object.prototype.hasOwnProperty.call(notification.data, key)) {
|
||||
const value = notification.data[key];
|
||||
if (typeof value === "object" && value !== null) {
|
||||
fcmData[key] = JSON.stringify(value);
|
||||
} else {
|
||||
fcmData[key] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notificationMessage: admin.messaging.Message = {
|
||||
notification: { title, body },
|
||||
data,
|
||||
android: {
|
||||
priority: 'high',
|
||||
notification: {
|
||||
channelId: 'notifications_channel',
|
||||
priority: 'high',
|
||||
defaultSound: true,
|
||||
defaultVibrateTimings: true,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
|
||||
},
|
||||
switch (notification.type) {
|
||||
case "trainer_response":
|
||||
title =
|
||||
notification.data?.title ||
|
||||
(notification.data?.status === "accepted"
|
||||
? "Trainer Invitation Accepted"
|
||||
: "Trainer Invitation Update");
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${
|
||||
notification.data?.trainerName
|
||||
} has ${notification.data?.status?.toLowerCase()} your request`;
|
||||
break;
|
||||
|
||||
case "trainer_assignment":
|
||||
title = notification.data?.title || "New Client Assignment";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`You have been assigned to train ${notification.data?.name}.`;
|
||||
break;
|
||||
|
||||
case "trainer_assigned_to_client":
|
||||
title = notification.data?.title || "Trainer Assigned";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.trainerName} has been assigned as your trainer.`;
|
||||
break;
|
||||
|
||||
case "trainer_update_owner":
|
||||
title = notification.data?.title || "Trainer Schedule Update";
|
||||
body =
|
||||
notification.data?.message || "A trainer has updated their schedule";
|
||||
break;
|
||||
|
||||
case "trainer_update_client":
|
||||
title = notification.data?.title || "Schedule Update";
|
||||
body =
|
||||
notification.data?.message || "Your training schedule has been updated";
|
||||
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
|
||||
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
|
||||
if (notification.data?.formattedDate) {
|
||||
body += ` on ${notification.data.formattedDate}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "plan_renewal":
|
||||
title = notification.data?.title || "Plan Renewal";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`Plan ${notification.data?.subscriptionName} has been renewed`;
|
||||
break;
|
||||
|
||||
case "plan_assigned":
|
||||
title = notification.data?.title || "New Plan Assigned";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`You have been assigned ${notification.data?.subscriptionName} at ${notification.data?.gymName}`;
|
||||
break;
|
||||
|
||||
case "plan_expired":
|
||||
title = notification.data?.title || "Plan Expired";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
|
||||
break;
|
||||
|
||||
case "plan_expiring_soon":
|
||||
title = notification.data?.title || "Plan Expiring Soon";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||
break;
|
||||
|
||||
case "trainer_client_plan_expired":
|
||||
title = notification.data?.title || "Client Plan Expired";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
|
||||
break;
|
||||
|
||||
case "trainer_client_plan_expiring":
|
||||
title = notification.data?.title || "Client Plan Expiring Soon";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||
break;
|
||||
|
||||
case "schedule_update":
|
||||
title = notification.data?.title || "Schedule Update";
|
||||
body =
|
||||
notification.data?.message || "Your training schedule has been updated";
|
||||
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
|
||||
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
|
||||
if (notification.data?.formattedDate) {
|
||||
body += ` on ${notification.data.formattedDate}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "attendance_dispute":
|
||||
title = notification.data?.title || "Attendance Dispute";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.name} has disputed an attendance record`;
|
||||
if (notification.data?.logTime) {
|
||||
body += ` for ${notification.data.logTime}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "day_pass_entry":
|
||||
const isAccepted = notification.data?.status === "ACCEPTED";
|
||||
title =
|
||||
notification.data?.title ||
|
||||
(isAccepted ? "Day Pass Approved" : "Day Pass Denied");
|
||||
body =
|
||||
notification.data?.message ||
|
||||
(isAccepted
|
||||
? "Your day pass has been approved"
|
||||
: "Your day pass has been denied");
|
||||
break;
|
||||
|
||||
case "client_invitations":
|
||||
if (notification.data?.userId || notification.data?.invitorId) {
|
||||
const isAccept = notification.data?.status === "ACCEPTED";
|
||||
title =
|
||||
notification.data?.title ||
|
||||
(isAccept ? "Invitation Accepted" : "Invitation Rejected");
|
||||
body =
|
||||
notification.data?.message ||
|
||||
(isAccept
|
||||
? `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been accepted`
|
||||
: `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been rejected`);
|
||||
} else if (notification.data?.phoneNumber) {
|
||||
const invitationStatus = getInvitationStatus(notification.data?.status);
|
||||
title =
|
||||
notification.data?.title || getInvitationTitle(invitationStatus);
|
||||
body =
|
||||
notification.data?.message ||
|
||||
getInvitationBody(invitationStatus, notification.data?.name);
|
||||
fcmData.status = invitationStatus;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.info(
|
||||
`Using default handling for notification type: ${notification.type}`
|
||||
);
|
||||
title =
|
||||
notification.data?.title ||
|
||||
(notification.type
|
||||
? `${notification.type.replace("_", " ").toUpperCase()}`
|
||||
: "Notification");
|
||||
break;
|
||||
}
|
||||
|
||||
const notificationMessage: admin.messaging.TokenMessage = {
|
||||
notification: { title, body },
|
||||
data: fcmData,
|
||||
android: {
|
||||
priority: "high",
|
||||
notification: {
|
||||
channelId: "notifications_channel",
|
||||
priority: "high",
|
||||
defaultSound: true,
|
||||
defaultVibrateTimings: true,
|
||||
icon: "@mipmap/ic_launcher",
|
||||
clickAction: "FLUTTER_NOTIFICATION_CLICK",
|
||||
},
|
||||
},
|
||||
apns: {
|
||||
payload: {
|
||||
aps: {
|
||||
sound: "default",
|
||||
badge: 1,
|
||||
},
|
||||
apns: {
|
||||
payload: {
|
||||
aps: {
|
||||
sound: 'default',
|
||||
badge: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
token: fcmToken,
|
||||
};
|
||||
return notificationMessage;
|
||||
},
|
||||
},
|
||||
token: fcmToken,
|
||||
};
|
||||
|
||||
logger.info(`Prepared notification: ${title} - ${body}`);
|
||||
return notificationMessage;
|
||||
}
|
||||
|
||||
function getInvitationStatus(status?: string): string {
|
||||
if (status === 'ACCEPTED') return 'accepted';
|
||||
if (status === 'REJECTED') return 'rejected';
|
||||
if (status === 'PENDING') return 'pending';
|
||||
return 'unknown';
|
||||
if (status === "ACCEPTED") return "accepted";
|
||||
if (status === "REJECTED") return "rejected";
|
||||
if (status === "PENDING") return "pending";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getInvitationTitle(status: string): string {
|
||||
switch (status) {
|
||||
case 'accepted': return 'Invitation Accepted';
|
||||
case 'rejected': return 'Invitation Rejected';
|
||||
case 'pending': return 'New Invitation';
|
||||
default: return 'Invitation Update';
|
||||
}
|
||||
switch (status) {
|
||||
case "accepted":
|
||||
return "Invitation Accepted";
|
||||
case "rejected":
|
||||
return "Invitation Rejected";
|
||||
case "pending":
|
||||
return "New Invitation";
|
||||
default:
|
||||
return "Invitation Update";
|
||||
}
|
||||
}
|
||||
|
||||
function getInvitationBody(status: string, name?: string): string {
|
||||
switch (status) {
|
||||
case 'accepted': return `You have accepted the invitation from ${name}`;
|
||||
case 'rejected': return `You have rejected the invitation from ${name}`;
|
||||
case 'pending': return `You have a new invitation pending from ${name}`;
|
||||
default: return 'There is an update to your invitation';
|
||||
}
|
||||
switch (status) {
|
||||
case "accepted":
|
||||
return `You have accepted the invitation from ${name}`;
|
||||
case "rejected":
|
||||
return `You have rejected the invitation from ${name}`;
|
||||
case "pending":
|
||||
return `You have a new invitation pending from ${name}`;
|
||||
default:
|
||||
return "There is an update to your invitation";
|
||||
}
|
||||
}
|
||||
|
||||
async function markNotificationAsSent(notificationId: string): Promise<void> {
|
||||
await app.firestore().collection('notifications').doc(notificationId).update({
|
||||
try {
|
||||
await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
.doc(notificationId)
|
||||
.update({
|
||||
notificationSent: true,
|
||||
sentAt: app.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
sentAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info(`Notification ${notificationId} marked as sent`);
|
||||
} catch (error) {
|
||||
logger.error(`Error marking notification as sent: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNotificationWithError(notificationId: string, error: string): Promise<void> {
|
||||
await app.firestore().collection('notifications').doc(notificationId).update({
|
||||
async function updateNotificationWithError(
|
||||
notificationId: string,
|
||||
error: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
.doc(notificationId)
|
||||
.update({
|
||||
notificationError: error,
|
||||
updatedAt: app.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
notificationSent: false,
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
logger.info(`Notification ${notificationId} marked with error: ${error}`);
|
||||
} catch (updateError) {
|
||||
logger.error(`Error updating notification with error: ${updateError}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
@ -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
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
@ -142,7 +142,7 @@ export const phonePeWebhook = onRequest({
|
||||
} 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',
|
||||
|
||||
@ -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,
|
||||
|
||||
242
package-lock.json
generated
242
package-lock.json
generated
@ -1,242 +0,0 @@
|
||||
{
|
||||
"name": "fitlien-services",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/pdfkit": "^0.13.9",
|
||||
"busboy": "^1.6.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"nodemailer": "^7.0.3",
|
||||
"pdfkit": "^0.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/busboy": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz",
|
||||
"integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
||||
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pdfkit": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz",
|
||||
"integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.12",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-exif": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="
|
||||
},
|
||||
"node_modules/linebreak": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||
"dependencies": {
|
||||
"base64-js": "0.0.8",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linebreak/node_modules/base64-js": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
|
||||
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
||||
},
|
||||
"node_modules/pdfkit": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz",
|
||||
"integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"jpeg-exif": "^1.1.4",
|
||||
"linebreak": "^1.1.0",
|
||||
"png-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/png-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
|
||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/busboy": "^1.5.4",
|
||||
"busboy": "^1.6.0",
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user