This commit is contained in:
paritosh18
2025-11-13 16:00:18 +05:30
13 changed files with 565 additions and 76 deletions

View File

@@ -0,0 +1,177 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { HostService } from '../../host/services/host.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import {
hostCompanyDetailsSchema,
REQUIRED_DOC_TYPES,
} from '../../../common/utils/validation/host/hostCompanyDetails.validation';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import config from '@/config/config';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
const s3 = new AWS.S3({
region: config.aws.region,
});
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
const userInfo = await verifyHostToken(token);
// ✅ 2. Ensure content-type is multipart/form-data
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.');
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
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.resume();
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
}
chunks.push(chunk);
});
file.on('end', () => {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
});
});
bb.on('field', (fieldname, val) => {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
});
bb.on('close', resolve);
bb.on('error', reject);
bb.end(bodyBuffer);
});
// ✅ 4. Validate 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.');
}
const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetails);
if (!companyValidation.success) {
const message = companyValidation.error.issues.map((e) => e.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.');
}
if (!Array.isArray(documentsMetadata) || documentsMetadata.length === 0)
throw new ApiError(400, 'Documents must be a non-empty array.');
// ✅ 5. Map uploaded files to document 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(', ')}`);
// ✅ 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}`;
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: doc.file.buffer,
ContentType: doc.file.mimeType,
ACL: 'private',
})
.promise();
uploadedDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
});
}
// ✅ 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.');
// ✅ 9. Success response
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Company details and documents uploaded successfully.',
data: createdHost,
}),
};
} catch (error: any) {
console.error('❌ Error in addCompanyDetails:', error);
throw error;
}
});

View File

@@ -5,6 +5,17 @@ import { AddPaymentDetailsDTO, CreateHostDto, UpdateHostDto } from '../dto/host.
import * as bcrypt from 'bcryptjs';
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';
type HostCompanyDetailsInput = z.infer<typeof hostCompanyDetailsSchema>;
// Document input after S3 upload (with S3 URL as filePath)
interface HostDocumentInput {
documentTypeXid: number;
documentName: string;
filePath: string; // S3 URL
}
@Injectable()
export class HostService {
@@ -224,4 +235,61 @@ export class HostService {
return addedPaymentDetails;
}
async addCompanyDetails(
companyData: HostCompanyDetailsInput,
documents: HostDocumentInput[] // Documents with S3 URLs
) {
return await this.prisma.$transaction(async (tx) => {
// ✅ Check for existing company
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
const createdHost = await tx.hostHeader.create({
data: {
companyName: companyData.companyName,
hostRefNumber: companyData.hostRefNumber,
address1: companyData.address1,
address2: companyData.address2,
cityXid: companyData.cityXid,
stateXid: companyData.stateXid,
countryXid: companyData.countryXid,
pinCode: companyData.pinCode,
logoPath: companyData.logoPath,
isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber,
gstNumber: companyData.gstNumber,
formationDate: new Date(companyData.formationDate),
companyType: companyData.companyType,
websiteUrl: companyData.websiteUrl,
instagramUrl: companyData.instagramUrl,
facebookUrl: companyData.facebookUrl,
linkedinUrl: companyData.linkedinUrl,
twitterUrl: companyData.twitterUrl,
currencyXid: companyData.currencyXid,
},
});
// ✅ Create documents (if provided)
if (documents && documents.length > 0) {
const docsData = documents.map((doc) => ({
hostXid: createdHost.id,
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: doc.filePath,
}));
await tx.hostDocuments.createMany({ data: docsData });
}
return createdHost;
});
}
}