made add parent company details and upload supporting documents
This commit is contained in:
@@ -7,6 +7,8 @@ import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
|
||||
import {
|
||||
hostCompanyDetailsSchema,
|
||||
REQUIRED_DOC_TYPES,
|
||||
hostDocumentsSchema,
|
||||
parentCompanySchema,
|
||||
} from '../../../common/utils/validation/host/hostCompanyDetails.validation';
|
||||
import AWS from 'aws-sdk';
|
||||
import Busboy from 'busboy';
|
||||
@@ -22,29 +24,21 @@ const s3 = new AWS.S3({
|
||||
|
||||
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
// ✅ 1. Verify Token
|
||||
// Extract token from headers
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Authenticate user using the shared authForHost function
|
||||
// 1) Auth
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
const userInfo = await verifyHostToken(token);
|
||||
|
||||
// ✅ 2. Ensure content-type is multipart/form-data
|
||||
// 2) multipart check
|
||||
const contentType = event.headers['content-type'] || event.headers['Content-Type'];
|
||||
if (!contentType?.startsWith('multipart/form-data'))
|
||||
throw new ApiError(400, 'Content-Type must be multipart/form-data.');
|
||||
|
||||
if (!event.isBase64Encoded)
|
||||
throw new ApiError(400, 'Event body must be base64 encoded for multipart uploads.');
|
||||
if (!contentType?.startsWith('multipart/form-data')) throw new ApiError(400, 'Content-Type must be multipart/form-data.');
|
||||
if (!event.isBase64Encoded) throw new ApiError(400, 'Event body must be base64 encoded for multipart uploads.');
|
||||
|
||||
const bodyBuffer = Buffer.from(event.body as string, 'base64');
|
||||
const fields: Record<string, any> = {};
|
||||
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
||||
|
||||
// ✅ 3. Parse multipart data using Busboy
|
||||
// 3) parse with Busboy
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const bb = Busboy({ headers: { 'content-type': contentType } });
|
||||
|
||||
@@ -74,6 +68,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
||||
});
|
||||
|
||||
bb.on('field', (fieldname, val) => {
|
||||
// Keep raw string for JSON parse later; try parse for convenience
|
||||
try {
|
||||
fields[fieldname] = JSON.parse(val);
|
||||
} catch {
|
||||
@@ -86,78 +81,143 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
||||
bb.end(bodyBuffer);
|
||||
});
|
||||
|
||||
// ✅ 4. Validate fields
|
||||
// 4) Validate required root fields
|
||||
if (!fields.companyDetails) throw new ApiError(400, 'Missing companyDetails field.');
|
||||
if (!fields.documents) throw new ApiError(400, 'Missing documents field.');
|
||||
if (files.length === 0) throw new ApiError(400, 'At least one document file is required.');
|
||||
|
||||
// ✅ Parse & validate JSON inputs
|
||||
let companyDetails;
|
||||
try {
|
||||
companyDetails = typeof fields.companyDetails === 'string' ? JSON.parse(fields.companyDetails) : fields.companyDetails;
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in companyDetails.');
|
||||
// 5) Parse companyDetails
|
||||
let companyDetailsRaw = fields.companyDetails;
|
||||
if (typeof companyDetailsRaw === 'string') {
|
||||
try {
|
||||
companyDetailsRaw = JSON.parse(companyDetailsRaw);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in companyDetails.');
|
||||
}
|
||||
}
|
||||
|
||||
const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetails);
|
||||
// 6) Zod validation for companyDetails (includes optional parentCompany)
|
||||
const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetailsRaw);
|
||||
if (!companyValidation.success) {
|
||||
const message = companyValidation.error.issues.map((e) => e.message).join(', ');
|
||||
const message = companyValidation.error.issues.map((i) => i.message).join(', ');
|
||||
throw new ApiError(400, `Validation failed: ${message}`);
|
||||
}
|
||||
const parsedCompany = companyValidation.data;
|
||||
|
||||
let documentsMetadata;
|
||||
try {
|
||||
documentsMetadata = typeof fields.documents === 'string' ? JSON.parse(fields.documents) : fields.documents;
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in documents.');
|
||||
// 7) Parse documents metadata
|
||||
let documentsMetadataRaw = fields.documents;
|
||||
if (typeof documentsMetadataRaw === 'string') {
|
||||
try {
|
||||
documentsMetadataRaw = JSON.parse(documentsMetadataRaw);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in documents.');
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(documentsMetadataRaw) || documentsMetadataRaw.length === 0) {
|
||||
throw new ApiError(400, 'Documents must be a non-empty array.');
|
||||
}
|
||||
|
||||
if (!Array.isArray(documentsMetadata) || documentsMetadata.length === 0)
|
||||
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' }
|
||||
const documentsMetadata = documentsMetadataRaw.map((d: any) => ({
|
||||
...d,
|
||||
owner: d.owner === 'parent' ? 'parent' : 'host',
|
||||
}));
|
||||
|
||||
// ✅ 5. Map uploaded files to document metadata
|
||||
// 9) Map uploaded files to metadata
|
||||
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}`);
|
||||
return { ...doc, file };
|
||||
});
|
||||
|
||||
// ✅ 6. Ensure all required document types exist
|
||||
const uploadedDocTypes = documentMetadata.map((d) => d.documentTypeXid);
|
||||
const missingDocs = Object.entries(REQUIRED_DOC_TYPES)
|
||||
.filter(([_, typeId]) => !uploadedDocTypes.includes(typeId))
|
||||
.map(([name]) => name);
|
||||
if (missingDocs.length > 0)
|
||||
throw new ApiError(400, `Missing mandatory documents: ${missingDocs.join(', ')}`);
|
||||
// 10) Split host vs parent docs
|
||||
const hostDocs = documentMetadata.filter((d) => d.owner === 'host');
|
||||
const parentDocs = documentMetadata.filter((d) => d.owner === 'parent');
|
||||
|
||||
// ✅ 7. Upload to S3
|
||||
const uploadedDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = [];
|
||||
for (const doc of documentMetadata) {
|
||||
const uniqueKey = `${userInfo.id}_${crypto.randomUUID()}_${doc.file.fileName}`;
|
||||
const s3Key = `Documents/Host/${uniqueKey}`;
|
||||
// 11) Ensure required docs for host exist
|
||||
const hostUploadedTypes = hostDocs.map((d) => d.documentTypeXid);
|
||||
const missingHostDocs = Object.entries(REQUIRED_DOC_TYPES)
|
||||
.filter(([_, typeId]) => !hostUploadedTypes.includes(typeId))
|
||||
.map(([name]) => name);
|
||||
if (missingHostDocs.length > 0) {
|
||||
throw new ApiError(400, `Missing mandatory documents for host: ${missingHostDocs.join(', ')}`);
|
||||
}
|
||||
|
||||
// 12) If isSubsidairy === true and parentCompany provided -> validate parent company & docs
|
||||
let parsedParentCompany: any = null;
|
||||
if (parsedCompany.isSubsidairy) {
|
||||
if (!parsedCompany.parentCompany) {
|
||||
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}`);
|
||||
}
|
||||
parsedParentCompany = parentValidation.data;
|
||||
|
||||
// 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);
|
||||
if (missingParentDocs.length > 0) {
|
||||
throw new ApiError(400, `Missing mandatory documents for parent company: ${missingParentDocs.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 13) Upload files to S3 (host docs under Documents/Host/, parent docs under Documents/Host/parent_company/)
|
||||
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}`;
|
||||
await s3
|
||||
.upload({
|
||||
Bucket: config.aws.bucketName,
|
||||
Key: s3Key,
|
||||
Body: doc.file.buffer,
|
||||
ContentType: doc.file.mimeType,
|
||||
Body: buffer,
|
||||
ContentType: mimeType,
|
||||
ACL: 'private',
|
||||
})
|
||||
.promise();
|
||||
|
||||
uploadedDocs.push({
|
||||
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
|
||||
}
|
||||
|
||||
// upload host files
|
||||
for (const doc of hostDocs) {
|
||||
const filePath = await uploadToS3(doc.file.buffer, doc.file.mimeType, doc.file.fileName, 'Documents/Host');
|
||||
uploadedHostDocs.push({
|
||||
documentTypeXid: doc.documentTypeXid,
|
||||
documentName: doc.documentName,
|
||||
filePath: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 8. Save company details + documents in DB via MinglarService
|
||||
const createdHost = await hostService.addCompanyDetails(parsedCompany, uploadedDocs);
|
||||
if (!createdHost) throw new ApiError(400, 'Failed to add company details.');
|
||||
// upload parent files (if any)
|
||||
if (parentDocs.length > 0) {
|
||||
for (const doc of parentDocs) {
|
||||
const filePath = await uploadToS3(doc.file.buffer, doc.file.mimeType, doc.file.fileName, 'Documents/Host/parent_company');
|
||||
uploadedParentDocs.push({
|
||||
documentTypeXid: doc.documentTypeXid,
|
||||
documentName: doc.documentName,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 9. Success response
|
||||
// 14) Persist using hostService (we updated service to accept optional parent info)
|
||||
const created = await hostService.addCompanyDetails(parsedCompany, uploadedHostDocs, parsedParentCompany, uploadedParentDocs);
|
||||
if (!created) throw new ApiError(400, 'Failed to add company details.');
|
||||
|
||||
// 15) Success
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
@@ -166,8 +226,8 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Company details and documents uploaded successfully.',
|
||||
data: createdHost,
|
||||
message: 'Company (and parent if provided) details and documents uploaded successfully.',
|
||||
data: created,
|
||||
}),
|
||||
};
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -7,6 +7,9 @@ import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { User } from '@prisma/client';
|
||||
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 } from '@/common/utils/constants/common.constant';
|
||||
|
||||
type HostCompanyDetailsInput = z.infer<typeof hostCompanyDetailsSchema>;
|
||||
|
||||
@@ -121,7 +124,7 @@ export class HostService {
|
||||
|
||||
async createHostUser(email: string) {
|
||||
const newUser = await this.prisma.user.create({
|
||||
data: { emailAddress: email, roleXid: 4 },
|
||||
data: { emailAddress: email, roleXid: ROLE.HOST },
|
||||
});
|
||||
return newUser;
|
||||
}
|
||||
@@ -177,19 +180,20 @@ export class HostService {
|
||||
|
||||
async addCompanyDetails(
|
||||
companyData: HostCompanyDetailsInput,
|
||||
documents: HostDocumentInput[] // Documents with S3 URLs
|
||||
documents: HostDocumentInput[], // host documents with S3 URLs
|
||||
parentCompanyData?: any | null, // optional parent company object
|
||||
parentDocuments?: HostDocumentInput[] // parent documents with S3 URLs
|
||||
) {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
// ✅ Check for existing company
|
||||
// 1) Check existing company by registrationNumber
|
||||
const existingHost = await tx.hostHeader.findFirst({
|
||||
where: { registrationNumber: companyData.registrationNumber },
|
||||
});
|
||||
|
||||
if (existingHost) {
|
||||
throw new ApiError(400, 'Company already exists with this registration number');
|
||||
}
|
||||
|
||||
// ✅ Create company record
|
||||
|
||||
// 2) Create host header record
|
||||
const createdHost = await tx.hostHeader.create({
|
||||
data: {
|
||||
companyName: companyData.companyName,
|
||||
@@ -213,10 +217,15 @@ export class HostService {
|
||||
linkedinUrl: companyData.linkedinUrl,
|
||||
twitterUrl: companyData.twitterUrl,
|
||||
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 documents (if provided)
|
||||
|
||||
// 3) Create host documents
|
||||
if (documents && documents.length > 0) {
|
||||
const docsData = documents.map((doc) => ({
|
||||
hostXid: createdHost.id,
|
||||
@@ -224,11 +233,52 @@ export class HostService {
|
||||
documentName: doc.documentName,
|
||||
filePath: doc.filePath,
|
||||
}));
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
// 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,
|
||||
}));
|
||||
|
||||
await tx.hostParenetDocuments.createMany({ data: parentDocsData });
|
||||
}
|
||||
}
|
||||
|
||||
return createdHost;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user