From 8e25c5c4a248311f198b2b2faf437d76f036ffb8 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Tue, 25 Nov 2025 12:02:15 +0530 Subject: [PATCH] made get all host applications for minglar admin and get all new host application for minglar admin --- prisma/schema.prisma | 47 ++-- serverless.yml | 66 +++-- src/common/utils/constants/common.constant.ts | 9 + .../utils/constants/minglar.constant.ts | 2 + .../host/handlers/addCompanyDetails.ts | 122 ++++----- src/modules/host/handlers/getByIdPQQ.ts | 13 +- src/modules/host/services/host.service.ts | 66 +++-- .../handlers/getAllOnboardingHosts.ts | 53 ++++ .../handlers/getOnboardingNewApplications.ts | 53 ++++ .../minglaradmin/services/minglar.service.ts | 235 +++++++++++++----- 10 files changed, 488 insertions(+), 178 deletions(-) create mode 100644 src/modules/minglaradmin/handlers/getAllOnboardingHosts.ts create mode 100644 src/modules/minglaradmin/handlers/getOnboardingNewApplications.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b16904c..458da16 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,15 +97,15 @@ model UserAddressDetails { } model UserDocuments { - id Int @id @default(autoincrement()) - userXid Int @map("user_xid") - user User @relation(fields: [userXid], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + userXid Int @map("user_xid") + user User @relation(fields: [userXid], references: [id], onDelete: Cascade) // documentTypeName String @map("document_type_name") @db.VarChar(50) - fileName String @map("file_name") @db.VarChar(500) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + fileName String @map("file_name") @db.VarChar(500) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("user_documents") @@schema("usr") @@ -655,7 +655,7 @@ model HostHeader { cities Cities @relation(fields: [cityXid], references: [id], onDelete: Restrict) stateXid Int @map("state_xid") states States @relation(fields: [stateXid], references: [id], onDelete: Restrict) - countryXid Int? @map("country_xid") + countryXid Int? @map("country_xid") countries Countries? @relation(fields: [countryXid], references: [id], onDelete: Restrict) pinCode String @map("pin_code") @db.VarChar(30) logoPath String? @map("logo_path") @db.VarChar(400) @@ -670,14 +670,14 @@ model HostHeader { facebookUrl String? @map("facebook_url") @db.VarChar(80) linkedinUrl String? @map("linkedin_url") @db.VarChar(80) twitterUrl String? @map("twitter_url") @db.VarChar(80) - currencyXid Int? @map("currency_xid") - currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict) - stepper Int? @default(1) @map("stepper") + currencyXid Int? @map("currency_xid") + currencies Currencies? @relation(fields: [currencyXid], references: [id], onDelete: Restrict) + stepper Int? @default(1) @map("stepper") hostStatusInternal String @default("pending") @map("host_status_internal") @db.VarChar(20) hostStatusDisplay String @default("pending") @map("host_status_Display") @db.VarChar(20) adminStatusInternal String @default("pending") @map("admin_status_internal") @db.VarChar(20) adminStatusDisplay String @default("pending") @map("admin_status_display") @db.VarChar(20) - amStatus String? @default("pending") @map("am_status") @db.VarChar(20) + amStatus String? @default("pending") @map("am_status") @db.VarChar(20) agreementAccepted Boolean @default(false) @map("agreement_accepted") accountManagerXid Int? @map("account_manager_xid") accountManager User? @relation("AccountManager", fields: [accountManagerXid], references: [id], onDelete: Restrict) @@ -819,16 +819,17 @@ model HostParenetDocuments { } model HostTrack { - id Int @id @default(autoincrement()) - hostXid Int @map("host_xid") - host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) - trackStatus String @map("track_status") @db.VarChar(20) - updatedByXid Int @map("updated_by_xid") - updatedBy User @relation(fields: [updatedByXid], references: [id], onDelete: Restrict) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + hostXid Int @map("host_xid") + host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) + updatedByRole String @map("updated_by_role") @db.VarChar(50) + trackStatus String @map("track_status") @db.VarChar(20) + updatedByXid Int @map("updated_by_xid") + updatedBy User @relation(fields: [updatedByXid], references: [id], onDelete: Restrict) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("host_track") @@schema("hst") diff --git a/serverless.yml b/serverless.yml index 56e2955..8dc01d6 100644 --- a/serverless.yml +++ b/serverless.yml @@ -5,12 +5,12 @@ provider: runtime: nodejs22.x region: ap-south-1 versionFunctions: false - memorySize: 512 # Default memory for all functions (can be overridden per function) + memorySize: 512 # Default memory for all functions (can be overridden per function) apiGateway: binaryMediaTypes: - '*/*' minimumCompressionSize: 1024 - + environment: DATABASE_URL: ${env:DATABASE_URL} DB_USERNAME: ${env:DB_USERNAME} @@ -80,7 +80,7 @@ package: functions: getHosts: handler: src/modules/host/handlers/host.handler - memorySize: 384 # Lower memory for simple GET operations + memorySize: 384 # Lower memory for simple GET operations package: patterns: - 'src/modules/host/handlers/host.*' @@ -339,7 +339,7 @@ functions: # Functions using AWS SDK - KEEP AS IS with higher memory updateMinglarProfile: handler: src/modules/minglaradmin/handlers/updateProfile.handler - memorySize: 512 # Higher memory for AWS SDK operations + memorySize: 512 # Higher memory for AWS SDK operations timeout: 30 package: patterns: @@ -387,7 +387,7 @@ functions: getAllHostApplication: handler: src/modules/minglaradmin/handlers/getAllHostApplication.handler - memorySize: 512 # Higher memory for data-intensive operations + memorySize: 512 # Higher memory for data-intensive operations package: patterns: - 'src/modules/minglaradmin/**' @@ -396,7 +396,35 @@ functions: - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' events: - httpApi: - path: /minglaradmin/get-all-host-applications + path: /minglaradmin/get-all-host-applications-am + method: get + + getAllOnboardingHostApplications: + handler: src/modules/minglaradmin/handlers/getAllOnboardingHosts.handler + memorySize: 512 # Higher memory for data-intensive operations + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'src/common/**' + - 'node_modules/@prisma/client/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + events: + - httpApi: + path: /minglaradmin/get-all-host-applications-admin + method: get + + getAllOnboardingHostApplications_New: + handler: src/modules/minglaradmin/handlers/getOnboardingNewApplications.handler + memorySize: 512 # Higher memory for data-intensive operations + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'src/common/**' + - 'node_modules/@prisma/client/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + events: + - httpApi: + path: /minglaradmin/get-all-host-applications-admin-new method: get getAllInvitationDetails: @@ -630,7 +658,7 @@ functions: # Functions using AWS SDK and S3 - KEEP AS IS with higher memory addCompanyDetails: handler: src/modules/host/handlers/addCompanyDetails.handler - memorySize: 512 # Higher memory for file upload operations + memorySize: 512 timeout: 30 package: patterns: @@ -639,18 +667,26 @@ functions: - 'src/common/**' - 'node_modules/@prisma/client/**' - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@aws-sdk/**' + # Only include specific AWS SDK modules needed for S3 + - 'node_modules/@aws-sdk/client-s3/**' + - 'node_modules/@aws-sdk/s3-request-presigner/**' + - 'node_modules/@aws-sdk/types/**' + - 'node_modules/@aws-sdk/middleware-logger/**' + - 'node_modules/@aws-sdk/util-utf8-node/**' + - 'node_modules/@aws-sdk/util-utf8-browser/**' - 'node_modules/@smithy/**' - 'node_modules/tslib/**' - - 'node_modules/fast-xml-parser/**' - - 'node_modules/lambda-multipart-parser/**' + # Remove these large/unnecessary packages: + - 'node_modules/fast-xml-parser/**' # Remove if not used + - 'node_modules/lambda-multipart-parser/**' # You're using busboy directly - 'node_modules/busboy/**' + # Remove these AWS utility packages (included in main SDK): - 'node_modules/@aws-crypto/**' - - 'node_modules/uuid/**' - - 'node_modules/@aws/util-uri-escape/**' - - 'node_modules/@aws/util-middleware/**' + # - 'node_modules/uuid/**' # AWS SDK includes its own + # - 'node_modules/@aws/util-uri-escape/**' + # - 'node_modules/@aws/util-middleware/**' - 'node_modules/@aws/smithy-client/**' - - 'node_modules/@aws/lambda-invoke-store/**' + # - 'node_modules/@aws/lambda-invoke-store/**' events: - httpApi: path: /host/add-company-details @@ -725,4 +761,4 @@ functions: events: - httpApi: path: /minglar/add-Pqq-suggestion - method: post \ No newline at end of file + method: post diff --git a/src/common/utils/constants/common.constant.ts b/src/common/utils/constants/common.constant.ts index b5fcb80..eec3934 100644 --- a/src/common/utils/constants/common.constant.ts +++ b/src/common/utils/constants/common.constant.ts @@ -7,6 +7,15 @@ export const ROLE = { USER: 6 } +export const ROLE_NAME = { + MINGLAR_ADMIN: 'Minglar Admin', + CO_ADMIN: 'Co-admin', + ACCOUNT_MANAGER: 'Account manager', + HOST: 'Host', + OPERATOR: 'Operator', + USER: 'User' +} + export const USER_STATUS = { INVITED: "Invited", ACTIVE: "Active", diff --git a/src/common/utils/constants/minglar.constant.ts b/src/common/utils/constants/minglar.constant.ts index a142291..896f4f5 100644 --- a/src/common/utils/constants/minglar.constant.ts +++ b/src/common/utils/constants/minglar.constant.ts @@ -5,6 +5,7 @@ export const MINGLAR_STATUS_INTERNAL = { AM_TO_REVIEW: 'AM To Review', AM_REJECTED: 'AM Rejected', AM_APPROVED: 'AM Approved', + DRAFT: 'Draft', }; export const MINGLAR_STATUS_DISPLAY = { @@ -14,6 +15,7 @@ export const MINGLAR_STATUS_DISPLAY = { ENHANCING: 'Enhancing', APPROVED: 'Approved', REJECTED: 'Rejected', + DRAFT: 'Draft' }; export const MINGLAR_INVITATION_STATUS = { diff --git a/src/modules/host/handlers/addCompanyDetails.ts b/src/modules/host/handlers/addCompanyDetails.ts index 4d61a2f..8ecde3c 100644 --- a/src/modules/host/handlers/addCompanyDetails.ts +++ b/src/modules/host/handlers/addCompanyDetails.ts @@ -65,7 +65,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const fields: Record = {}; const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = []; - // 3) parse with Busboy - FIXED VERSION + // 3) parse with Busboy await new Promise((resolve, reject) => { const bb = Busboy({ headers: { @@ -105,7 +105,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< }); bb.on('field', (fieldname, val) => { - // Store as string initially, parse later in normalizeJsonField fields[fieldname] = val; }); @@ -121,6 +120,9 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< bb.end(); }); + // Extract isDraft flag from fields (default to false if not provided) + const isDraft = fields.isDraft === 'true' || fields.isDraft === true; + if (fields.userProfile) { const userProfileRaw = normalizeJsonField(fields, "userProfile"); if (userProfileRaw) { @@ -157,39 +159,31 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< if (existingHost) { hostId = existingHost.id; } else { - // For new hosts, we'll use user ID temporarily and update after host creation hostId = userInfo.id; } - // Define uploadToS3 function with proper folder structure using fieldName for filenames - // Define uploadToS3 function with proper folder structure using fieldName for filenames + // Define uploadToS3 function (same as before) async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, folderType: 'logo' | 'documents' | 'parent_company', documentTypeXid?: number, fieldName?: string) { let s3Key: string; - // Sanitize file name: remove special characters and spaces const sanitizeFileName = (name: string) => { return name .toLowerCase() - .replace(/[^a-z0-9.]/g, '_') // Replace special characters with underscore - .replace(/_+/g, '_') // Replace multiple underscores with single - .replace(/^_+|_+$/g, ''); // Remove leading/trailing underscores + .replace(/[^a-z0-9.]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); }; - // Get file extension from original file name const fileExtension = originalName.split('.').pop() || 'pdf'; - // Determine folder structure based on type if (folderType === 'logo') { - // Logo: Documents/Host/logo/{HostID}/{sanitized_filename} const sanitizedFileName = sanitizeFileName(originalName); s3Key = `Documents/Host/${hostId}/logo/${sanitizedFileName}`; } else if (folderType === 'documents' && documentTypeXid && fieldName) { - // Host Documents: Documents/Host/documents/{HostID}/{documentTypeXid}_{fieldName}.{extension} const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`; const sanitizedFileName = sanitizeFileName(fileName); s3Key = `Documents/Host/${hostId}/documents/${sanitizedFileName}`; } else if (folderType === 'parent_company' && documentTypeXid && fieldName) { - // Parent Documents: Documents/Host/parent_company/{HostID}/{documentTypeXid}_{fieldName}.{extension} const fileName = `${documentTypeXid}_${fieldName}.${fileExtension}`; const sanitizedFileName = sanitizeFileName(fileName); s3Key = `Documents/Host/${hostId}/parent_company/${sanitizedFileName}`; @@ -197,7 +191,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< throw new ApiError(400, 'Invalid folder type or missing documentTypeXid/fieldName'); } - // Upload new file (S3 will automatically replace if same key exists) await s3 .upload({ Bucket: config.aws.bucketName, @@ -225,7 +218,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< console.log('Company logo uploaded:', logoPath); } - // 6) Zod validation for companyDetails (includes optional parentCompany) + // 6) Zod validation for companyDetails const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetailsRaw); if (!companyValidation.success) { const message = companyValidation.error.issues.map((i) => i.message).join(', '); @@ -246,7 +239,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } const documentsMetadata = documentsMetadataRaw.map((d: any) => ({ ...d, - owner: d.owner || 'host', // default to host + owner: d.owner || 'host', })); // 8) Map uploaded files to metadata @@ -260,12 +253,14 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const hostDocs = documentMetadata.filter((d) => d.owner === 'host'); const parentDocs = documentMetadata.filter((d) => d.owner === 'parent'); - // 10) Ensure required docs for host exist (IDs 1,2,3,4) - const hostUploadedTypes = hostDocs.map((d) => d.documentTypeXid); - const requiredHostTypes = Object.values(REQUIRED_DOC_TYPES); - const missingHostDocs = requiredHostTypes.filter((typeId) => !hostUploadedTypes.includes(typeId)); - if (missingHostDocs.length > 0) { - throw new ApiError(400, `Missing mandatory documents for host: ${missingHostDocs.join(', ')}`); + // 10) If NOT draft, validate required documents + if (!isDraft) { + const hostUploadedTypes = hostDocs.map((d) => d.documentTypeXid); + const requiredHostTypes = Object.values(REQUIRED_DOC_TYPES); + const missingHostDocs = requiredHostTypes.filter((typeId) => !hostUploadedTypes.includes(typeId)); + if (missingHostDocs.length > 0) { + throw new ApiError(400, `Missing mandatory documents for host: ${missingHostDocs.join(', ')}`); + } } // 11) If isSubsidairy === true and parentCompany provided -> validate parent company & docs @@ -282,61 +277,65 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } parsedParentCompany = parsedCompany.parentCompany; - const parentUploadedTypes = parentDocs.map((d) => d.documentTypeXid); - const requiredParentTypes = Object.values(REQUIRED_DOC_TYPES); - const missingParentDocs = requiredParentTypes.filter((typeId) => !parentUploadedTypes.includes(typeId)); - if (missingParentDocs.length > 0) { - throw new ApiError(400, `Missing mandatory documents for parent company: ${missingParentDocs.join(', ')}`); + // If NOT draft, validate required parent documents + if (!isDraft) { + const parentUploadedTypes = parentDocs.map((d) => d.documentTypeXid); + const requiredParentTypes = Object.values(REQUIRED_DOC_TYPES); + const missingParentDocs = requiredParentTypes.filter((typeId) => !parentUploadedTypes.includes(typeId)); + if (missingParentDocs.length > 0) { + throw new ApiError(400, `Missing mandatory documents for parent company: ${missingParentDocs.join(', ')}`); + } } } - // 12) Upload files to S3 with proper folder structure using fieldName for filenames + // 12) Upload files to S3 (same for both draft and final submission) const uploadedHostDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; const uploadedParentDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; - // Upload host documents with proper folder structure using fieldName + // Upload host documents for (const doc of hostDocs) { const filePath = await uploadToS3( doc.file.buffer, doc.file.mimeType, - doc.file.fileName, // Use original file name for extension + doc.file.fileName, 'documents', doc.documentTypeXid, - doc.fieldName // Use fieldName for the filename + doc.fieldName ); uploadedHostDocs.push({ documentTypeXid: doc.documentTypeXid, - documentName: doc.fieldName, // Keep documentName for database + documentName: doc.fieldName, filePath, }); } - // Upload parent company documents with proper folder structure using fieldName + // Upload parent company documents if (parentDocs.length > 0) { for (const doc of parentDocs) { const filePath = await uploadToS3( doc.file.buffer, doc.file.mimeType, - doc.file.fileName, // Use original file name for extension + doc.file.fileName, 'parent_company', doc.documentTypeXid, - doc.fieldName // Use fieldName for the filename + doc.fieldName ); uploadedParentDocs.push({ documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, // Keep documentName for database + documentName: doc.documentName, filePath, }); } } - // 13) Persist using hostService + // 13) Persist using hostService - PASS isDraft flag const createdOrUpdated = await hostService.addOrUpdateCompanyDetails( userInfo.id, parsedCompany, uploadedHostDocs, parsedParentCompany, - uploadedParentDocs + uploadedParentDocs, + isDraft // Pass the isDraft flag ); if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); @@ -347,25 +346,28 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< console.log(`Host created with ID: ${hostId}`); } - const getSuggestionDetails = await hostService.getSuggestionDetails(userInfo.id) + // 14) Send emails only for FINAL submission (not draft) + if (!isDraft) { + const getSuggestionDetails = await hostService.getSuggestionDetails(userInfo.id) - if (getSuggestionDetails.hostDetails.accountManagerXid !== null) { - await sendEmailToAM( - getSuggestionDetails.hostDetails.accountManager.emailAddress, - getSuggestionDetails.hostDetails.accountManager.firstName, - getSuggestionDetails.hostDetails.companyName, - getSuggestionDetails.hostDetails.hostRefNumber - ); - } else { - await sendEmailToMinglarAdmin( - config.MinglarAdminEmail, - config.MinglarAdminName, - getSuggestionDetails.hostDetails.companyName, - getSuggestionDetails.hostDetails.hostRefNumber - ) + if (getSuggestionDetails.hostDetails.accountManagerXid !== null) { + await sendEmailToAM( + getSuggestionDetails.hostDetails.accountManager.emailAddress, + getSuggestionDetails.hostDetails.accountManager.firstName, + getSuggestionDetails.hostDetails.companyName, + getSuggestionDetails.hostDetails.hostRefNumber + ); + } else { + await sendEmailToMinglarAdmin( + config.MinglarAdminEmail, + config.MinglarAdminName, + getSuggestionDetails.hostDetails.companyName, + getSuggestionDetails.hostDetails.hostRefNumber + ) + } } - // 14) Success + // 15) Success response return { statusCode: 200, headers: { @@ -374,8 +376,14 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< }, body: JSON.stringify({ success: true, - message: 'Company (and parent if provided) details and documents uploaded successfully.', - data: { id: createdOrUpdated.id, hostRefNumber: (createdOrUpdated as any).hostRefNumber } + message: isDraft + ? 'Company details saved as draft successfully.' + : 'Company (and parent if provided) details and documents uploaded successfully.', + data: { + id: createdOrUpdated.id, + hostRefNumber: (createdOrUpdated as any).hostRefNumber, + isDraft + } }), }; } catch (error: any) { diff --git a/src/modules/host/handlers/getByIdPQQ.ts b/src/modules/host/handlers/getByIdPQQ.ts index def1575..8c016d4 100644 --- a/src/modules/host/handlers/getByIdPQQ.ts +++ b/src/modules/host/handlers/getByIdPQQ.ts @@ -22,17 +22,10 @@ export const handler = safeHandler(async ( const userInfo = await verifyMinglarAdminHostToken(token); const userId = Number(userInfo.id); - let body: { question_xid: number, activity_xid: number }; + const question_xid = Number(event.queryStringParameters?.question_xid); + const activity_xid = Number(event.queryStringParameters?.activity_xid); - try { - body = event.body ? JSON.parse(event.body) : {}; - } catch (error) { - throw new ApiError(400, 'Invalid JSON in request body'); - } - - const { question_xid, activity_xid } = body; - - if(!question_xid || !activity_xid){ + if (!question_xid || !activity_xid) { throw new ApiError(400, "Question and activity xid are required.") } diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index b77b655..6fddf52 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; import { hostCompanyDetailsSchema } from '@/common/utils/validation/host/hostCompanyDetails.validation'; import { HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, STEPPER } from '@/common/utils/constants/host.constant'; import { MINGLAR_STATUS_DISPLAY, MINGLAR_STATUS_INTERNAL } from '@/common/utils/constants/minglar.constant'; -import { ROLE, USER_STATUS } from '@/common/utils/constants/common.constant'; +import { ROLE, ROLE_NAME, USER_STATUS } from '@/common/utils/constants/common.constant'; import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl'; import config from '@/config/config'; @@ -281,7 +281,8 @@ export class HostService { companyData: HostCompanyDetailsInput, documents: HostDocumentInput[], parentCompanyData?: any | null, - parentDocuments?: HostDocumentInput[] + parentDocuments?: HostDocumentInput[], + isDraft: boolean = false // Add isDraft parameter with default false ) { return await this.prisma.$transaction(async (tx) => { // Check if host already has a company @@ -290,13 +291,32 @@ export class HostService { include: { hostParent: true }, }); + // Determine status based on isDraft flag + const hostStatusInternal = isDraft + ? HOST_STATUS_INTERNAL.DRAFT + : HOST_STATUS_INTERNAL.HOST_SUBMITTED; + + const hostStatusDisplay = isDraft + ? HOST_STATUS_DISPLAY.DRAFT + : HOST_STATUS_DISPLAY.UNDER_REVIEW; + + const minglarStatusInternal = isDraft + ? MINGLAR_STATUS_INTERNAL.DRAFT + : MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; + + const minglarStatusDisplay = isDraft + ? MINGLAR_STATUS_DISPLAY.DRAFT + : MINGLAR_STATUS_DISPLAY.NEW; + // CREATE if (!existingHostCompany) { - // Optionally check unique registration number - const existingByPan = await tx.hostHeader.findFirst({ - where: { panNumber: companyData.panNumber }, - }); - if (existingByPan) throw new ApiError(400, 'Company already exists with this pan/bin number'); + // Optionally check unique registration number (only for final submission) + if (!isDraft) { + const existingByPan = await tx.hostHeader.findFirst({ + where: { panNumber: companyData.panNumber }, + }); + if (existingByPan) throw new ApiError(400, 'Company already exists with this pan/bin number'); + } const refNumber = await this.generateHostRefNumber(tx); @@ -323,12 +343,11 @@ export class HostService { facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, - // currencyXid: companyData.currencyXid, stepper: STEPPER.UNDER_REVIEW, - hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, + hostStatusInternal: hostStatusInternal, + hostStatusDisplay: hostStatusDisplay, + adminStatusInternal: minglarStatusInternal, + adminStatusDisplay: minglarStatusDisplay, }, }); @@ -385,7 +404,6 @@ export class HostService { } // UPDATE existing - // Prevent changing hostRefNumber const updatedHost = await tx.hostHeader.update({ where: { id: existingHostCompany.id }, data: { @@ -408,9 +426,11 @@ export class HostService { facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, - // currencyXid: companyData.currencyXid, - stepper: STEPPER.UNDER_REVIEW - // hostRefNumber: DO NOT UPDATE + stepper: STEPPER.UNDER_REVIEW, + hostStatusInternal: hostStatusInternal, + hostStatusDisplay: hostStatusDisplay, + adminStatusInternal: minglarStatusInternal, + adminStatusDisplay: minglarStatusDisplay }, }); @@ -428,7 +448,6 @@ export class HostService { // Parent company create/update and replace parent docs if (companyData.isSubsidairy) { - // existingHostCompany.hostParent may be array or single object depending on Prisma schema let parentRecord = (existingHostCompany as any).hostParent; if (Array.isArray(parentRecord)) parentRecord = parentRecord[0]; @@ -519,6 +538,19 @@ export class HostService { } } + const hostDetails = await this.prisma.hostHeader.findFirst({ + where: { userXid: user_xid }, + }) + + await this.prisma.hostTrack.create({ + data: { + hostXid: hostDetails.id, + updatedByRole: ROLE_NAME.HOST, + updatedByXid: user_xid, + trackStatus: hostDetails.hostStatusInternal, + } + }) + return updatedHost; }); } diff --git a/src/modules/minglaradmin/handlers/getAllOnboardingHosts.ts b/src/modules/minglaradmin/handlers/getAllOnboardingHosts.ts new file mode 100644 index 0000000..bb17d6e --- /dev/null +++ b/src/modules/minglaradmin/handlers/getAllOnboardingHosts.ts @@ -0,0 +1,53 @@ +import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { MinglarService } from '../services/minglar.service'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +/** + * Get all host applications handler + * Returns host details with status, submission date, and account manager info + */ +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Verify authentication token + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError(401, 'This is a protected route. Please provide a valid token.'); + } + + // Verify token and get user info + const userInfo = await verifyOnlyMinglarAdminToken(token); + + // Get user details including role + const user = await prismaService.user.findUnique({ + where: { id: userInfo.id }, + select: { id: true, roleXid: true } + }); + + if (!user) { + throw new ApiError(404, 'User not found'); + } + + // Get all host applications from service based on user role + const hostApplications = await minglarService.getAllOnboardingHostApplications(); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Host applications retrieved successfully', + data: hostApplications, + }), + }; +}); diff --git a/src/modules/minglaradmin/handlers/getOnboardingNewApplications.ts b/src/modules/minglaradmin/handlers/getOnboardingNewApplications.ts new file mode 100644 index 0000000..34ca161 --- /dev/null +++ b/src/modules/minglaradmin/handlers/getOnboardingNewApplications.ts @@ -0,0 +1,53 @@ +import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { MinglarService } from '../services/minglar.service'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +/** + * Get all host applications handler + * Returns host details with status, submission date, and account manager info + */ +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Verify authentication token + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']; + if (!token) { + throw new ApiError(401, 'This is a protected route. Please provide a valid token.'); + } + + // Verify token and get user info + const userInfo = await verifyOnlyMinglarAdminToken(token); + + // Get user details including role + const user = await prismaService.user.findUnique({ + where: { id: userInfo.id }, + select: { id: true, roleXid: true } + }); + + if (!user) { + throw new ApiError(404, 'User not found'); + } + + // Get all host applications from service based on user role + const hostApplications = await minglarService.getAllOnboardingHostApplications_New(); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Host applications retrieved successfully', + data: hostApplications, + }), + }; +}); diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index edeba64..126b501 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -1,4 +1,4 @@ -import { ROLE, USER_STATUS } from '@/common/utils/constants/common.constant'; +import { ROLE, ROLE_NAME, USER_STATUS } from '@/common/utils/constants/common.constant'; import { HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, @@ -19,7 +19,7 @@ import { sendAMEmailForHostAssign } from './AMEmail.service'; @Injectable() export class MinglarService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService) { } async createPassword(user_xid: number, password: string): Promise { // Find user by id @@ -597,7 +597,7 @@ export class MinglarService { userStatus && userStatus.trim().toLowerCase() === MINGLAR_STATUS_DISPLAY.NEW.toLowerCase() ) { - filters.adminStatusDisplay = MINGLAR_STATUS_DISPLAY.NEW; + filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; } /** ----------------------------------- @@ -670,6 +670,85 @@ export class MinglarService { })); } + async getAllOnboardingHostApplications() { + return await this.prisma.hostHeader.findMany({ + where: { isActive: true }, + select: { + id: true, + hostRefNumber: true, + companyName: true, + adminStatusDisplay: true, + assignedOn: true, + accountManagerXid: true, + createdAt: true, + user: { + select: { + id: true, + firstName: true, + lastName: true, + } + }, + accountManager: { + select: { + id: true, + firstName: true, + lastName: true, + profileImage: true + } + } + } + }) + } + + + async getAllOnboardingHostApplications_New() { + return await this.prisma.hostHeader.findMany({ + where: { isActive: true, adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW }, + select: { + id: true, + hostRefNumber: true, + companyName: true, + adminStatusDisplay: true, + assignedOn: true, + accountManagerXid: true, + createdAt: true, + cities: { + select: { + id: true, + cityName: true + } + }, + countries: { + select: { + id: true, + countryName: true, + } + }, + states: { + select: { + id: true, + stateName: true + } + }, + user: { + select: { + id: true, + firstName: true, + lastName: true, + } + }, + accountManager: { + select: { + id: true, + firstName: true, + lastName: true, + profileImage: true + } + } + } + }) + } + async getAllCoadminAndAM() { // 1. Fetch all required users (Admin, Co-Admin, AM) @@ -770,7 +849,7 @@ export class MinglarService { if ( hostDetails.adminStatusInternal !== - MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED && + MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED && hostDetails.adminStatusDisplay !== MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED ) { throw new ApiError(400, 'Invalid host status'); @@ -931,41 +1010,65 @@ export class MinglarService { } async acceptHostApplication(host_xid: number, user_xid: number) { - return await this.prisma.hostHeader.update({ - where: { - id: host_xid, - hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW, - }, - data: { - hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED, - hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED, - stepper: STEPPER.COMPANY_DETAILS_APPROVED, - }, - }); + return await this.prisma.$transaction(async (tx) => { + await this.prisma.hostHeader.update({ + where: { + id: host_xid, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW, + }, + data: { + hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED, + hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED, + stepper: STEPPER.COMPANY_DETAILS_APPROVED, + }, + }); + + await this.prisma.hostTrack.create({ + data: { + hostXid: host_xid, + updatedByRole: ROLE_NAME.ACCOUNT_MANAGER, + updatedByXid: user_xid, + trackStatus: MINGLAR_STATUS_INTERNAL.AM_APPROVED, + } + }) + }) + + } async acceptHostApplicationMinglarAdmin(host_xid: number, user_xid: number) { - return await this.prisma.hostHeader.update({ - where: { - id: host_xid, - hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, - }, - data: { - isApproved: true, - hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED, - }, - }); + return await this.prisma.$transaction(async (tx) => { + await tx.hostHeader.update({ + where: { + id: host_xid, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, + }, + data: { + isApproved: true, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED, + }, + }); + + await this.prisma.hostTrack.create({ + data: { + hostXid: host_xid, + updatedByRole: ROLE_NAME.MINGLAR_ADMIN, + updatedByXid: user_xid, + trackStatus: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, + } + }) + }) } async rejectHostApplication(host_xid: number, user_xid: number) { @@ -991,6 +1094,15 @@ export class MinglarService { }, }); + await this.prisma.hostTrack.create({ + data: { + hostXid: hostDetails.id, + updatedByRole: ROLE_NAME.MINGLAR_ADMIN, + updatedByXid: user_xid, + trackStatus: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED, + } + }) + await tx.user.update({ where: { id: hostDetails.userXid }, data: { @@ -1001,25 +1113,36 @@ export class MinglarService { } async rejectHostApplicationAM(host_xid: number, user_xid: number) { - const hostDetails = await this.prisma.hostHeader.findFirst({ - where: { id: host_xid }, - select: { id: true, userXid: true }, - }); - if (!hostDetails) { - throw new Error('Host not found'); - } - await this.prisma.hostHeader.update({ - where: { - id: host_xid, - hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, - }, - data: { - hostStatusInternal: HOST_STATUS_INTERNAL.REJECTED, - hostStatusDisplay: HOST_STATUS_DISPLAY.REJECTED, - adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED, - adminStatusDisplay: MINGLAR_STATUS_DISPLAY.REJECTED, - }, - }); + return await this.prisma.$transaction(async (tx) => { + const hostDetails = await this.prisma.hostHeader.findFirst({ + where: { id: host_xid }, + select: { id: true, userXid: true }, + }); + if (!hostDetails) { + throw new Error('Host not found'); + } + await this.prisma.hostHeader.update({ + where: { + id: host_xid, + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, + hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, + }, + data: { + hostStatusInternal: HOST_STATUS_INTERNAL.HOST_TO_UPDATE, + hostStatusDisplay: HOST_STATUS_DISPLAY.ENHANCING, + adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_REJECTED, + adminStatusDisplay: MINGLAR_STATUS_DISPLAY.ENHANCING, + }, + }); + + await this.prisma.hostTrack.create({ + data: { + hostXid: hostDetails.id, + updatedByRole: ROLE_NAME.ACCOUNT_MANAGER, + updatedByXid: user_xid, + trackStatus: MINGLAR_STATUS_INTERNAL.AM_REJECTED, + } + }) + }) } }