From db65abf69314fc6fe1bc1d2880519e8e102b15b7 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Mon, 17 Nov 2025 19:05:26 +0530 Subject: [PATCH] fixed the add company details api --- serverless.yml | 2 +- .../host/hostCompanyDetails.validation.ts | 7 +- .../host/handlers/addCompanyDetails.ts | 62 ++-- src/modules/host/services/host.service.ts | 272 +++++++++++++----- 4 files changed, 241 insertions(+), 102 deletions(-) diff --git a/serverless.yml b/serverless.yml index 52937a9..a8fea9a 100644 --- a/serverless.yml +++ b/serverless.yml @@ -356,4 +356,4 @@ functions: events: - httpApi: path: /host/add-company-details - method: post + method: patch diff --git a/src/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts index 8470941..418835f 100644 --- a/src/common/utils/validation/host/hostCompanyDetails.validation.ts +++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts @@ -42,7 +42,6 @@ export const parentCompanySchema = z.object({ linkedinUrl: z.string().url().optional(), twitterUrl: z.string().url().optional(), - currencyXid: z.number().min(1, "Currency is required"), }); @@ -50,7 +49,6 @@ export const hostCompanyDetailsSchema = z.object({ companyName: z.string().min(1, "Company name is required"), address1: z.string().min(1, "Address1 is required"), address2: z.string().optional(), - hostRefNumber: z.string().min(1, "Host reference number is required"), cityXid: z.number().min(1, "City is required"), stateXid: z.number().min(1, "State is required"), countryXid: z.number().min(1,"Country is required"), @@ -80,7 +78,8 @@ export const hostDocumentsSchema = z.array( z.object({ documentTypeXid: z.number(), documentName: z.string(), - fieldName: z.string(), // metadata must include the fieldName so we can map files - owner: z.enum(['host', 'parent']).optional(), // optional, default to host + fieldName: z.string(), // maps to the multipart file field + owner: z.enum(['host', 'parent']).optional(), // default to host + isOptional: z.boolean().optional(), // optional docs flag if frontend provides it }) ); diff --git a/src/modules/host/handlers/addCompanyDetails.ts b/src/modules/host/handlers/addCompanyDetails.ts index a5ab281..311c911 100644 --- a/src/modules/host/handlers/addCompanyDetails.ts +++ b/src/modules/host/handlers/addCompanyDetails.ts @@ -1,3 +1,4 @@ +// modules/host/handlers/addCompanyDetails.ts import config from '@/config/config'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import AWS from 'aws-sdk'; @@ -10,7 +11,8 @@ import ApiError from '../../../common/utils/helper/ApiError'; import { hostCompanyDetailsSchema, parentCompanySchema, - REQUIRED_DOC_TYPES + REQUIRED_DOC_TYPES, + hostDocumentsSchema } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; import { HostService } from '../../host/services/host.service'; @@ -26,10 +28,8 @@ function normalizeJsonField(fields: any, key: string) { const val = fields[key]; - // If frontend sends object if (typeof val === "object") return val; - // If Postman or CURL sends stringified JSON if (typeof val === "string") { try { return JSON.parse(val); @@ -41,7 +41,6 @@ function normalizeJsonField(fields: any, key: string) { throw new ApiError(400, `Invalid input: ${key} must be object or JSON string.`); } - export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise => { try { // 1) Auth @@ -109,7 +108,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const companyDetailsRaw = normalizeJsonField(fields, "companyDetails"); if (!companyDetailsRaw) throw new ApiError(400, "companyDetails is required."); - // 6) Zod validation for companyDetails (includes optional parentCompany) const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetailsRaw); if (!companyValidation.success) { @@ -120,20 +118,21 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< // 7) Parse documents metadata const documentsMetadataRaw = normalizeJsonField(fields, "documents"); - if (!Array.isArray(documentsMetadataRaw)) - throw new ApiError(400, "documents must be an array."); - if (!Array.isArray(documentsMetadataRaw) || documentsMetadataRaw.length === 0) { - throw new ApiError(400, 'Documents must be a non-empty array.'); - } + if (!Array.isArray(documentsMetadataRaw)) throw new ApiError(400, "documents must be an array."); + if (!documentsMetadataRaw.length) throw new ApiError(400, 'Documents must be a non-empty array.'); - // 8) Accept documents metadata with optional owner field (host | parent). Default owner: 'host' - // Expected doc shape: { documentTypeXid, documentName, fieldName, owner?: 'host' | 'parent' } + // Validate documents metadata shape + const docsParse = hostDocumentsSchema.safeParse(documentsMetadataRaw); + if (!docsParse.success) { + const message = docsParse.error.issues.map((i) => i.message).join(', '); + throw new ApiError(400, `Documents validation failed: ${message}`); + } const documentsMetadata = documentsMetadataRaw.map((d: any) => ({ ...d, - owner: d.owner === 'parent' ? 'parent' : 'host', + owner: d.owner === 'parent' ? 'parent' : 'host', // default host })); - // 9) Map uploaded files to metadata + // 9) Map uploaded files to metadata (one entry per file - Q2 = A) const documentMetadata = documentsMetadata.map((doc: any) => { const file = files.find((f) => f.fieldName === doc.fieldName); if (!file) throw new ApiError(400, `File not found for field: ${doc.fieldName}`); @@ -144,11 +143,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const hostDocs = documentMetadata.filter((d) => d.owner === 'host'); const parentDocs = documentMetadata.filter((d) => d.owner === 'parent'); - // 11) Ensure required docs for host exist + // 11) Ensure required docs for host exist (IDs 1,2,3,4) const hostUploadedTypes = hostDocs.map((d) => d.documentTypeXid); - const missingHostDocs = Object.entries(REQUIRED_DOC_TYPES) - .filter(([_, typeId]) => !hostUploadedTypes.includes(typeId)) - .map(([name]) => name); + 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(', ')}`); } @@ -160,16 +158,13 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< throw new ApiError(400, 'isSubsidairy is true but parentCompany object is missing inside companyDetails.'); } - // Validate parent company with the same company schema (or create a dedicated parent schema if needed) const parentValidation = parentCompanySchema.safeParse(parsedCompany.parentCompany); - - if (!parentValidation.success) { const message = parentValidation.error.issues.map((i) => i.message).join(', '); throw new ApiError(400, `Parent company validation failed: ${message}`); } - let parentCompanyRaw = parsedCompany.parentCompany; + let parentCompanyRaw = parsedCompany.parentCompany; if (typeof parentCompanyRaw === "string") { try { parentCompanyRaw = JSON.parse(parentCompanyRaw); @@ -177,15 +172,11 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< throw new ApiError(400, "Invalid JSON in parentCompany."); } } - parsedParentCompany = parentCompanyRaw; - - // Ensure required parent docs exist const parentUploadedTypes = parentDocs.map((d) => d.documentTypeXid); - const missingParentDocs = Object.entries(REQUIRED_DOC_TYPES) - .filter(([_, typeId]) => !parentUploadedTypes.includes(typeId)) - .map(([name]) => name); + 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(', ')}`); } @@ -195,7 +186,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< const uploadedHostDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; const uploadedParentDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; - // helper uploader async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string) { const uniqueKey = `${userInfo.id}_${crypto.randomUUID()}_${originalName}`; const s3Key = `${prefix}/${uniqueKey}`; @@ -234,9 +224,16 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise< } } - // 14) Persist using hostService (we updated service to accept optional parent info) - const created = await hostService.addCompanyDetails(userInfo.id, parsedCompany, uploadedHostDocs, parsedParentCompany, uploadedParentDocs); - if (!created) throw new ApiError(400, 'Failed to add company details.'); + // 14) Persist using hostService + const createdOrUpdated = await hostService.addOrUpdateCompanyDetails( + userInfo.id, + parsedCompany, + uploadedHostDocs, + parsedParentCompany, + uploadedParentDocs + ); + + if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); // 15) Success return { @@ -248,6 +245,7 @@ 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 } }), }; } catch (error: any) { diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index b68779b..11176cd 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -189,59 +189,148 @@ export class HostService { return addedPaymentDetails; } - async addCompanyDetails( + async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, - documents: HostDocumentInput[], // host documents with S3 URLs - parentCompanyData?: any | null, // optional parent company object - parentDocuments?: HostDocumentInput[] // parent documents with S3 URLs + documents: HostDocumentInput[], + parentCompanyData?: any | null, + parentDocuments?: HostDocumentInput[] ) { return await this.prisma.$transaction(async (tx) => { - // 1) Check existing company by registrationNumber - const existingHost = await tx.hostHeader.findFirst({ - where: { registrationNumber: companyData.registrationNumber }, + // Check if host already has a company + const existingHostCompany = await tx.hostHeader.findFirst({ + where: { userXid: user_xid }, + include: { hostParent: true }, }); - if (existingHost) { - throw new ApiError(400, 'Company already exists with this registration number'); + + // CREATE + if (!existingHostCompany) { + // Optionally check unique registration number + const existingByReg = await tx.hostHeader.findFirst({ + where: { registrationNumber: companyData.registrationNumber }, + }); + if (existingByReg) throw new ApiError(400, 'Company already exists with this registration number'); + + const refNumber = await this.generateHostRefNumber(tx); + + const createdHost = await tx.hostHeader.create({ + data: { + userXid: user_xid, + companyName: companyData.companyName, + hostRefNumber: refNumber, + address1: companyData.address1, + address2: companyData.address2, + cityXid: companyData.cityXid, + stateXid: companyData.stateXid, + countryXid: companyData.countryXid, + pinCode: companyData.pinCode, + logoPath: companyData.logoPath || null, + isSubsidairy: companyData.isSubsidairy, + registrationNumber: companyData.registrationNumber, + panNumber: companyData.panNumber, + gstNumber: companyData.gstNumber || null, + formationDate: new Date(companyData.formationDate), + companyType: companyData.companyType, + websiteUrl: companyData.websiteUrl || null, + instagramUrl: companyData.instagramUrl || null, + 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, + }, + }); + + // Create host documents + if (documents?.length) { + const docsData = documents.map((doc) => ({ + hostXid: createdHost.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostDocuments.createMany({ data: docsData }); + } + + // Parent company and its docs (if present) + if (companyData.isSubsidairy && parentCompanyData) { + const createdParent = await tx.hostParent.create({ + data: { + hostXid: createdHost.id, + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1, + address2: parentCompanyData.address2 || null, + cityXid: parentCompanyData.cityXid, + stateXid: parentCompanyData.stateXid, + countryXid: parentCompanyData.countryXid, + pinCode: parentCompanyData.pinCode, + logoPath: parentCompanyData.logoPath || null, + isSubsidairy: false, + registrationNumber: parentCompanyData.registrationNumber, + panNumber: parentCompanyData.panNumber, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: new Date(parentCompanyData.formationDate), + companyType: parentCompanyData.companyType, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); + + if (parentDocuments?.length) { + const parentDocsData = parentDocuments.map((doc) => ({ + hostParentXid: createdParent.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostParenetDocuments.createMany({ data: parentDocsData }); + } + } + + return createdHost; } - // 2) Create host header record - const createdHost = await tx.hostHeader.create({ + // UPDATE existing + // Prevent changing hostRefNumber + const updatedHost = await tx.hostHeader.update({ + where: { id: existingHostCompany.id }, data: { - userXid: user_xid, companyName: companyData.companyName, - hostRefNumber: await this.generateHostRefNumber(tx), address1: companyData.address1, address2: companyData.address2, cityXid: companyData.cityXid, stateXid: companyData.stateXid, countryXid: companyData.countryXid, pinCode: companyData.pinCode, - logoPath: companyData.logoPath, + logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, - gstNumber: companyData.gstNumber, + gstNumber: companyData.gstNumber || null, formationDate: new Date(companyData.formationDate), companyType: companyData.companyType, - websiteUrl: companyData.websiteUrl, - instagramUrl: companyData.instagramUrl, - facebookUrl: companyData.facebookUrl, - linkedinUrl: companyData.linkedinUrl, - twitterUrl: companyData.twitterUrl, + websiteUrl: companyData.websiteUrl || null, + instagramUrl: companyData.instagramUrl || null, + 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, + // hostRefNumber: DO NOT UPDATE }, }); - // 3) Create host documents - if (documents && documents.length > 0) { + // Replace host documents (delete old, insert new) + await tx.hostDocuments.deleteMany({ where: { hostXid: updatedHost.id } }); + if (documents?.length) { const docsData = documents.map((doc) => ({ - hostXid: createdHost.id, + hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, @@ -249,51 +338,104 @@ export class HostService { await tx.hostDocuments.createMany({ data: docsData }); } - // 4) If subsidiary and parentCompanyData present -> create parent record + parent docs - if (companyData.isSubsidairy && parentCompanyData) { - // create HostParent with link to createdHost.id (hostXid) - const createdParent = await tx.hostParent.create({ - data: { - hostXid: createdHost.id, - companyName: parentCompanyData.companyName, - address1: parentCompanyData.address1, - address2: parentCompanyData.address2 || null, - cityXid: parentCompanyData.cityXid, - stateXid: parentCompanyData.stateXid, - countryXid: parentCompanyData.countryXid, - pinCode: parentCompanyData.pinCode, - logoPath: parentCompanyData.logoPath || null, - isSubsidairy: false, // parent itself is not marked as subsidiary here - registrationNumber: parentCompanyData.registrationNumber, - panNumber: parentCompanyData.panNumber, - gstNumber: parentCompanyData.gstNumber || null, - formationDate: new Date(parentCompanyData.formationDate), - companyType: parentCompanyData.companyType, - websiteUrl: parentCompanyData.websiteUrl || null, - instagramUrl: parentCompanyData.instagramUrl || null, - facebookUrl: parentCompanyData.facebookUrl || null, - linkedinUrl: parentCompanyData.linkedinUrl || null, - twitterUrl: parentCompanyData.twitterUrl || null, - }, - }); + // 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]; - // create parent documents - if (parentDocuments && parentDocuments.length > 0) { - const parentDocsData = parentDocuments.map((doc) => ({ - hostParentXid: createdParent.id, - documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath: doc.filePath, - })); + if (!parentRecord) { + // create + const createdParent = await tx.hostParent.create({ + data: { + hostXid: updatedHost.id, + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1, + address2: parentCompanyData.address2 || null, + cityXid: parentCompanyData.cityXid, + stateXid: parentCompanyData.stateXid, + countryXid: parentCompanyData.countryXid, + pinCode: parentCompanyData.pinCode, + logoPath: parentCompanyData.logoPath || null, + isSubsidairy: false, + registrationNumber: parentCompanyData.registrationNumber, + panNumber: parentCompanyData.panNumber, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: new Date(parentCompanyData.formationDate), + companyType: parentCompanyData.companyType, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); - await tx.hostParenetDocuments.createMany({ data: parentDocsData }); + if (parentDocuments?.length) { + const parentDocsData = parentDocuments.map((doc) => ({ + hostParentXid: createdParent.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostParenetDocuments.createMany({ data: parentDocsData }); + } + } else { + // update + await tx.hostParent.update({ + where: { id: parentRecord.id }, + data: { + companyName: parentCompanyData.companyName, + address1: parentCompanyData.address1, + address2: parentCompanyData.address2 || null, + cityXid: parentCompanyData.cityXid, + stateXid: parentCompanyData.stateXid, + countryXid: parentCompanyData.countryXid, + pinCode: parentCompanyData.pinCode, + logoPath: parentCompanyData.logoPath || null, + registrationNumber: parentCompanyData.registrationNumber, + panNumber: parentCompanyData.panNumber, + gstNumber: parentCompanyData.gstNumber || null, + formationDate: new Date(parentCompanyData.formationDate), + companyType: parentCompanyData.companyType, + websiteUrl: parentCompanyData.websiteUrl || null, + instagramUrl: parentCompanyData.instagramUrl || null, + facebookUrl: parentCompanyData.facebookUrl || null, + linkedinUrl: parentCompanyData.linkedinUrl || null, + twitterUrl: parentCompanyData.twitterUrl || null, + }, + }); + + // replace parent docs + await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: parentRecord.id } }); + if (parentDocuments?.length) { + const parentDocsData = parentDocuments.map((doc) => ({ + hostParentXid: parentRecord.id, + documentTypeXid: doc.documentTypeXid, + documentName: doc.documentName, + filePath: doc.filePath, + })); + await tx.hostParenetDocuments.createMany({ data: parentDocsData }); + } + } + } else { + // If previously had a parent and now isSubsidairy=false -> optionally delete parent and its docs + const previousParent = (existingHostCompany as any).hostParent; + let prevParentId = null; + if (Array.isArray(previousParent) && previousParent.length) prevParentId = previousParent[0].id; + else if (previousParent && previousParent.id) prevParentId = previousParent.id; + + if (prevParentId) { + await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: prevParentId } }); + await tx.hostParent.delete({ where: { id: prevParentId } }); } } - return createdHost; + return updatedHost; }); } + async generateHostRefNumber(tx: any) { const lastHost = await tx.hostHeader.findFirst({ orderBy: {