fixed the add company details api

This commit is contained in:
2025-11-17 19:05:26 +05:30
parent 7ed225a3f0
commit db65abf693
4 changed files with 241 additions and 102 deletions

View File

@@ -356,4 +356,4 @@ functions:
events:
- httpApi:
path: /host/add-company-details
method: post
method: patch

View File

@@ -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
})
);

View File

@@ -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<APIGatewayProxyResult> => {
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) {

View File

@@ -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: {