From 3ac0a591df15124eff5e93a195cf8f96f4bc749c Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Thu, 13 Nov 2025 17:48:09 +0530 Subject: [PATCH] made add parent company details and upload supporting documents --- serverless.yml | 132 +++++++------- src/common/middlewares/jwt/authForHost.ts | 3 +- .../middlewares/jwt/authForMinglarAdmin.ts | 5 +- src/common/middlewares/jwt/authForUser.ts | 3 +- src/common/utils/constants/common.constant.ts | 8 + src/common/utils/constants/host.constant.ts | 24 +++ .../utils/constants/minglar.constant.ts | 17 ++ .../host/hostCompanyDetails.validation.ts | 59 +++++- .../host/handlers/addCompanyDetails.ts | 170 ++++++++++++------ src/modules/host/services/host.service.ts | 70 ++++++-- 10 files changed, 342 insertions(+), 149 deletions(-) create mode 100644 src/common/utils/constants/common.constant.ts create mode 100644 src/common/utils/constants/host.constant.ts create mode 100644 src/common/utils/constants/minglar.constant.ts diff --git a/serverless.yml b/serverless.yml index 82f61fe..2813bbb 100644 --- a/serverless.yml +++ b/serverless.yml @@ -3,12 +3,14 @@ service: minglarDev provider: name: aws runtime: nodejs20.x - apiGateway: - binaryMediaTypes: - - '*/*' # allow binary uploads - minimumCompressionSize: 0 region: ap-south-1 versionFunctions: false + + apiGateway: + binaryMediaTypes: + - "*/*" + minimumCompressionSize: 0 + environment: DATABASE_URL: ${env:DATABASE_URL} DB_USERNAME: ${env:DB_USERNAME} @@ -39,58 +41,54 @@ provider: - s3:PutObject - s3:GetObject - s3:DeleteObject - Resource: 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*' + Resource: "arn:aws:s3:::${env:S3_BUCKET_NAME}/*" - httpApi: - cors: - allowedOrigins: ['*'] - allowedHeaders: - - Content-Type - - X-Amz-Date - - Authorization - - X-Api-Key - - X-Auth-Token - allowCredentials: false - +# ------------------------------------------------------------ +# ESBUILD — bundles AWS SDK v3 and removes node_modules bloat +# ------------------------------------------------------------ custom: esbuild: bundle: true - minify: false + minify: true sourcemap: false - exclude: [] target: node20 platform: node concurrency: 10 - outdir: dist + # ❗ Prisma stays inside functions, so exclude only Prisma engine structure + exclude: + - ".prisma" # keeps native engine files untouched + - "@aws-sdk" # prevents double bundling + + +# ------------------------------------------------------------ +# GLOBAL PACKAGE SETTINGS +# ------------------------------------------------------------ package: individually: true patterns: - - '!**/*.spec.ts' - - '!**/*.test.ts' - - '!**/*.log' - - 'src/**' - - 'common/**' - - 'prisma/schema.prisma' - - 'node_modules/@prisma/client/**' - - 'node_modules/.prisma/client/**' - - 'node_modules/@aws-sdk/**' - - 'node_modules/@smithy/**' # ✅ include AWS SDK v3 internal deps - - 'node_modules/@aws-crypto/**' + - "!node_modules/aws-sdk/**" # AWS SDK v2 NOT needed in Node 20 + - "!**/*.test.ts" + - "!**/*.spec.ts" + - "!**/*.log" + - "!dist/**" + - "!prisma/**" # prisma schema not needed in deployment + +# ------------------------------------------------------------ +# LAMBDA FUNCTIONS — Prisma included per-function +# ------------------------------------------------------------ functions: - # 👇 Example Lambda for Host Module getHosts: handler: src/modules/host/handlers/host.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host @@ -100,11 +98,10 @@ functions: handler: src/modules/host/handlers/verifyOtp.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host/verify-otp @@ -114,11 +111,10 @@ functions: handler: src/modules/host/handlers/loginForHost.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host/login @@ -128,11 +124,10 @@ functions: handler: src/modules/host/handlers/registration.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host/registration @@ -142,26 +137,23 @@ functions: handler: src/modules/host/handlers/createPassword.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host/create-password method: post - addPaymentDetailsForHost: handler: src/modules/host/handlers/addPaymentDetails.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" events: - httpApi: path: /host/add-payment-details @@ -171,15 +163,13 @@ functions: handler: src/modules/host/handlers/addCompanyDetails.handler package: patterns: - - 'src/modules/host/**' - - 'common/**' - - 'node_modules/@aws-sdk/**' - - 'node_modules/@smithy/**' # ✅ critical fix - - 'node_modules/@aws-crypto/**' - - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' - - 'node_modules/@prisma/client/**' - - 'prisma/schema.prisma' + - "src/modules/host/**" + - "common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" + - "node_modules/@smithy/**" + - "node_modules/@aws-sdk/**" events: - httpApi: path: /host/add-company-details - method: post \ No newline at end of file + method: post diff --git a/src/common/middlewares/jwt/authForHost.ts b/src/common/middlewares/jwt/authForHost.ts index bc12c77..834f8ee 100644 --- a/src/common/middlewares/jwt/authForHost.ts +++ b/src/common/middlewares/jwt/authForHost.ts @@ -4,6 +4,7 @@ import { Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; import ApiError from '../../utils/helper/ApiError'; import config from '../../../config/config'; +import { ROLE } from '@/common/utils/constants/common.constant'; const prisma = new PrismaClient(); @@ -60,7 +61,7 @@ export async function verifyHostToken(token: string): Promise<{ id: number; role } // ✅ Check Host role (role_xid = 4) - if (user.roleXid !== 4) { + if (user.roleXid !== ROLE.HOST) { throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only'); } diff --git a/src/common/middlewares/jwt/authForMinglarAdmin.ts b/src/common/middlewares/jwt/authForMinglarAdmin.ts index 8fb6f9c..2318433 100644 --- a/src/common/middlewares/jwt/authForMinglarAdmin.ts +++ b/src/common/middlewares/jwt/authForMinglarAdmin.ts @@ -4,6 +4,7 @@ import { Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; import ApiError from '../../utils/helper/ApiError'; import config from '../../../config/config'; +import { ROLE } from '@/common/utils/constants/common.constant'; const prisma = new PrismaClient(); @@ -57,7 +58,7 @@ export async function verifyMinglarAdminToken(token: string): Promise<{ id: numb } // ✅ Check Host role (role_xid = 1) - if (user.roleXid !== 1) { + if (user.roleXid !== ROLE.MINGLAR_ADMIN) { throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only'); } @@ -76,7 +77,7 @@ export async function verifyMinglarAdminToken(token: string): Promise<{ id: numb } /** - * Verifies JWT and validates Host user (role_xid = 4) + * Verifies JWT and validates Host user (role_xid = 1) */ const verifyCallback = async ( req: Request, diff --git a/src/common/middlewares/jwt/authForUser.ts b/src/common/middlewares/jwt/authForUser.ts index ed2cae3..a6eaf66 100644 --- a/src/common/middlewares/jwt/authForUser.ts +++ b/src/common/middlewares/jwt/authForUser.ts @@ -4,6 +4,7 @@ import { Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; import ApiError from '../../utils/helper/ApiError'; import config from '../../../config/config'; +import { ROLE } from '@/common/utils/constants/common.constant'; const prisma = new PrismaClient(); @@ -57,7 +58,7 @@ export async function verifyUserToken(token: string): Promise<{ id: number; role } // ✅ Check Host role (role_xid = 6) - if (user.roleXid !== 6) { + if (user.roleXid !== ROLE.USER) { throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only'); } diff --git a/src/common/utils/constants/common.constant.ts b/src/common/utils/constants/common.constant.ts new file mode 100644 index 0000000..05f9f6d --- /dev/null +++ b/src/common/utils/constants/common.constant.ts @@ -0,0 +1,8 @@ +export const ROLE = { + MINGLAR_ADMIN: 1, + CO_ADMIN: 2, + ACCOUNT_MANAGER: 3, + HOST: 4, + OPERATOR: 5, + USER: 6 +} \ No newline at end of file diff --git a/src/common/utils/constants/host.constant.ts b/src/common/utils/constants/host.constant.ts new file mode 100644 index 0000000..32ef20c --- /dev/null +++ b/src/common/utils/constants/host.constant.ts @@ -0,0 +1,24 @@ +export const HOST_STATUS_INTERNAL = { + HOST_SUBMITTED: "Host Submitted", + HOST_TO_UPDATE: "Host To Update", + REJECTED: "Rejected", + APPROVED: "Approved", + DRAFT: "Draft", +} + +export const HOST_STATUS_DISPLAY = { + DRAFT: "Draft", + UNDER_REVIEW: "Under Review", + ENHANCING: "Enhancing", + REJECTED: "Rejected", + APPROVED: "Approved", +} + +export const STEPPER = { + NOT_SUBMITTED: 1, + UNDER_REVIEW: 2, + COMPANY_DETAILS_APPROVED: 3, + BANK_DETAILS_UPDATED: 4, + AGREEMENT_ACCEPTED: 5, + REJECTED: 6 +} \ No newline at end of file diff --git a/src/common/utils/constants/minglar.constant.ts b/src/common/utils/constants/minglar.constant.ts new file mode 100644 index 0000000..0c1077f --- /dev/null +++ b/src/common/utils/constants/minglar.constant.ts @@ -0,0 +1,17 @@ +export const MINGLAR_STATUS_INTERNAL = { + ADMIN_TO_REVIEW: "Admin To Review", + ADMIN_REJECTED: "Admin Rejected", + AM_NOT_ASSIGNED: "AM Not Assigned", + AM_TO_REVIEW: "AM To Review", + AM_REJECTED: "AM Rejected", + AM_APPROVED: "AM Approved" +} + +export const MINGLAR_STATUS_DISPLAY = { + NEW: "New", + AM_NOT_ASSIGNED: "AM Not Assigned", + TO_REVIEW: "To Review", + ENHANCING: "Enhancing", + APPROVED: "Approved", + REJECTED: "Rejected", +} \ No newline at end of file diff --git a/src/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts index c37ad80..8470941 100644 --- a/src/common/utils/validation/host/hostCompanyDetails.validation.ts +++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -// Allowed document types (must match your DocumentType master table IDs) +// REQUIRED_DOC_TYPES (use your mapping) export const REQUIRED_DOC_TYPES = { GST: 1, PAN: 2, @@ -8,6 +8,44 @@ export const REQUIRED_DOC_TYPES = { AADHAAR: 4, }; +export const parentCompanySchema = z.object({ + companyName: z.string().min(1, "Parent company name is required"), + address1: z.string().min(1, "Address1 is required"), + address2: z.string().optional(), + + // Parent companies DO NOT need this + hostRefNumber: z.string().optional(), + + cityXid: z.number().min(1, "City is required"), + stateXid: z.number().min(1, "State is required"), + countryXid: z.number().min(1, "Country is required"), + + pinCode: z.string().min(4, "Pincode/Zipcode is required"), + + logoPath: z.string().optional(), + + // Parent companies do NOT need this + isSubsidairy: z.boolean().optional(), + + registrationNumber: z.string().min(1, "Registration number is required"), + panNumber: z.string().min(1, "PAN number is required"), + gstNumber: z.string().optional(), + + formationDate: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Formation date must be a valid date", + }), + + companyType: z.string().min(1, "Company type is required"), + websiteUrl: z.string().url().optional(), + instagramUrl: z.string().url().optional(), + facebookUrl: z.string().url().optional(), + linkedinUrl: z.string().url().optional(), + twitterUrl: z.string().url().optional(), + + currencyXid: z.number().min(1, "Currency is required"), +}); + + export const hostCompanyDetailsSchema = z.object({ companyName: z.string().min(1, "Company name is required"), address1: z.string().min(1, "Address1 is required"), @@ -26,20 +64,23 @@ export const hostCompanyDetailsSchema = z.object({ message: "Formation date must be a valid date", }), companyType: z.string().min(1, "Company type is required"), - websiteUrl: z.url().optional(), - instagramUrl: z.url().optional(), - facebookUrl: z.url().optional(), - linkedinUrl: z.url().optional(), - twitterUrl: z.url().optional(), + websiteUrl: z.string().url().optional(), + instagramUrl: z.string().url().optional(), + facebookUrl: z.string().url().optional(), + linkedinUrl: z.string().url().optional(), + twitterUrl: z.string().url().optional(), currencyXid: z.number().min(1, "Currency is required"), + + // Parent company nested when this is subsidiary + parentCompany: parentCompanySchema.optional(), }); -// Validation for documents with file data (base64) +// Documents schema: added optional owner export const hostDocumentsSchema = z.array( z.object({ documentTypeXid: z.number(), documentName: z.string(), - fileData: z.string().min(1, "File data is required"), // base64 encoded file - contentType: z.string().optional(), // e.g., "application/pdf", "image/png" + fieldName: z.string(), // metadata must include the fieldName so we can map files + owner: z.enum(['host', 'parent']).optional(), // optional, default to host }) ); diff --git a/src/modules/host/handlers/addCompanyDetails.ts b/src/modules/host/handlers/addCompanyDetails.ts index 4f955a0..a7ded32 100644 --- a/src/modules/host/handlers/addCompanyDetails.ts +++ b/src/modules/host/handlers/addCompanyDetails.ts @@ -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 => { 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 = {}; const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = []; - // ✅ 3. Parse multipart data using Busboy + // 3) parse with Busboy await new Promise((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) { diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 1302b8d..d9f5824 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -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; @@ -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; }); } + }