made add parent company details and upload supporting documents

This commit is contained in:
2025-11-13 17:48:09 +05:30
parent 72f9e26ca6
commit 3ac0a591df
10 changed files with 342 additions and 149 deletions

View File

@@ -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
method: post

View File

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

View File

@@ -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,

View File

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

View File

@@ -0,0 +1,8 @@
export const ROLE = {
MINGLAR_ADMIN: 1,
CO_ADMIN: 2,
ACCOUNT_MANAGER: 3,
HOST: 4,
OPERATOR: 5,
USER: 6
}

View File

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

View File

@@ -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",
}

View File

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

View File

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

View File

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