diff --git a/package.json b/package.json index 08d884c..933385a 100644 --- a/package.json +++ b/package.json @@ -48,20 +48,27 @@ "@types/http-status": "^1.1.2", "ajv": "8.12.0", "aws-lambda": "^1.0.7", + "aws-sdk": "^2.1692.0", "bcrypt": "^6.0.0", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "date-fns": "^4.1.0", + "dayjs": "^1.11.19", + "docx": "^9.6.0", + "docxtemplater": "^3.68.3", "fast-xml-parser": "^5.3.1", "fs": "^0.0.1-security", "helmet": "^7.1.0", "http-status": "^2.1.0", "moment": "^2.30.1", + "number-to-words": "^1.2.4", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "path": "^0.12.7", + "pdf-lib": "^1.17.1", + "pizzip": "^3.2.0", "prisma": "^7.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 2179901..186911c 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -28,6 +28,13 @@ import { import ApiError from '../../../common/utils/helper/ApiError'; import { hostCompanyDetailsSchema } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; import config from '../../../config/config'; +import AWS from 'aws-sdk'; +import dayjs from 'dayjs'; +import { toWords } from 'number-to-words'; +import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; +import { Document, Packer, Paragraph, TextRun } from 'docx'; +import PizZip from 'pizzip'; +import Docxtemplater from 'docxtemplater'; import { CreateActivityInput } from '../dto/createActivity.schema'; import { AddPaymentDetailsDTO, @@ -105,6 +112,260 @@ function computeBasePriceAndTaxes( const normalize = (v?: string | null) => v ? v.trim().toLowerCase() : null; +const s3 = new AWS.S3({ region: config.aws.region }); + +async function getTemplateFromS3() { + const obj = await s3 + .getObject({ + Bucket: config.aws.bucketName, + Key: 'Documents/MinglarAdmin/AgreementDoc/CopyofMinglarHostAgreement.docx', + }) + .promise(); + + return obj.Body as Buffer; +} + +async function generateDocxFromTemplate(buffer: Buffer, data: any) { + const zip = new PizZip(buffer); + const doc = new Docxtemplater(zip); + + doc.setData({ + companyName: data.companyName, + companyType: data.companyType, + fullAddress: data.fullAddress, + effectiveDate: data.effectiveDate, + expiryDate: data.expiryDate, + durationText: data.durationText, + commissionText: data.commissionText, + acceptDate: data.acceptDate, + }); + + doc.render(); + + return doc.getZip().generate({ type: 'nodebuffer' }); +} + +function buildFullAddress(host: any) { + return [ + host.address1, + host.address2, + host.cities?.cityName || host.cities?.name, + host.states?.stateName || host.states?.name, + host.countries?.countryName || host.countries?.name, + host.pinCode, + ] + .filter(Boolean) + .join(', '); +} + +function buildDurationText(host: any) { + if (!host.durationNumber || !host.durationFrequency) { + throw new ApiError(400, 'Duration not configured'); + } + const numberWord = toWords(host.durationNumber); + const freq = + host.durationFrequency === 'month' && host.durationNumber > 1 + ? 'months' + : host.durationFrequency === 'year' && host.durationNumber > 1 + ? 'years' + : host.durationFrequency; + + return `${numberWord} (${host.durationNumber}) ${freq}`; +} + +function buildExpiryDate(host: any) { + if (!host.agreementStartDate) { + throw new ApiError(400, 'Agreement start date missing'); + } + + return dayjs(host.agreementStartDate) + .add(host.durationNumber, host.durationFrequency) + .format('DD-MMM-YY'); +} + +function buildCommissionText(host: any) { + if (host.isCommisionBase) { + if (!host.commisionPer) { + throw new ApiError(400, 'Commission % missing'); + } + return `${host.commisionPer}% commission`; + } + + if (!host.amountPerBooking) { + throw new ApiError(400, 'Per booking amount missing'); + } + + return `₹${host.amountPerBooking} per booking`; +} + +async function generateAgreementPdfBuffer(vars: { + effectiveDate: string; + companyName: string; + companyType?: string | null; + fullAddress: string; + durationText: string; + expiryDate: string; + commissionText: string; + acceptDate: string; +}) { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage([595, 842]); // A4 + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + const { width, height } = page.getSize(); + const margin = 50; + let cursorY = height - margin; + const lineHeight = 18; + + const drawLine = (text: string, bold = false) => { + page.drawText(text, { + x: margin, + y: cursorY, + size: 12, + font: bold ? fontBold : font, + color: rgb(0, 0, 0), + }); + cursorY -= lineHeight; + }; + + // Title + page.drawText('HOST AGREEMENT', { + x: margin, + y: cursorY, + size: 16, + font: fontBold, + color: rgb(0, 0, 0), + }); + cursorY -= lineHeight * 2; + + drawLine(`Effective Date: ${vars.effectiveDate}`, true); + drawLine(`Host Legal Name: ${vars.companyName}`); + if (vars.companyType) { + drawLine(`Company Type: ${vars.companyType}`); + } + drawLine(`Address: ${vars.fullAddress}`); + cursorY -= lineHeight; + + drawLine(`Agreement Duration: ${vars.durationText}`); + drawLine(`Expiry Date: ${vars.expiryDate}`); + drawLine(`Commercials: ${vars.commissionText}`); + cursorY -= lineHeight * 2; + + drawLine('Host:', true); + drawLine(vars.companyName); + drawLine(`Date of Acceptance: ${vars.acceptDate}`); + + const pdfBytes = await pdfDoc.save(); + return Buffer.from(pdfBytes); +} + +async function generateAgreementDocxBuffer(vars: { + effectiveDate: string; + companyName: string; + companyType?: string | null; + fullAddress: string; + durationText: string; + expiryDate: string; + commissionText: string; + acceptDate: string; +}) { + const paragraphs: Paragraph[] = []; + + paragraphs.push( + new Paragraph({ + children: [ + new TextRun({ + text: 'HOST AGREEMENT', + bold: true, + size: 32, + }), + ], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [ + new TextRun({ + text: `Effective Date: ${vars.effectiveDate}`, + bold: true, + }), + ], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [ + new TextRun(`Host Legal Name: ${vars.companyName}`), + ], + }), + ); + + if (vars.companyType) { + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Company Type: ${vars.companyType}`)], + }), + ); + } + + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Address: ${vars.fullAddress}`)], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Agreement Duration: ${vars.durationText}`)], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Expiry Date: ${vars.expiryDate}`)], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Commercials: ${vars.commissionText}`)], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun({ text: 'Host:', bold: true })], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun(vars.companyName)], + }), + ); + + paragraphs.push( + new Paragraph({ + children: [new TextRun(`Date of Acceptance: ${vars.acceptDate}`)], + }), + ); + + const doc = new Document({ + sections: [ + { + properties: {}, + children: paragraphs, + }, + ], + }); + + const buffer = await Packer.toBuffer(doc); + return buffer; +} + const findOrCreateCountry = async ( tx: any, countryName?: string | null, @@ -685,20 +946,103 @@ export class HostService { } async acceptMinglarAgreement(user_xid: number) { - const hostDetails = await this.prisma.hostHeader.findFirst({ - where: { userXid: user_xid }, - select: { - id: true, - userXid: true, + const host = await this.prisma.hostHeader.findFirst({ + where: { userXid: user_xid, isActive: true }, + include: { + cities: true, + states: true, + countries: true, + companyTypes: true, }, }); - await this.prisma.hostHeader.update({ - where: { id: hostDetails.id }, - data: { - stepper: STEPPER.AGREEMENT_ACCEPTED, - isApproved: true, - agreementAccepted: true, - }, + + if (!host) { + throw new ApiError(404, 'Host not found for this user'); + } + + const effectiveDate = host.agreementStartDate + ? dayjs(host.agreementStartDate).format('DD-MMM-YY') + : dayjs().format('DD-MMM-YY'); + + const durationText = buildDurationText(host); + const expiryDate = buildExpiryDate(host); + const commissionText = buildCommissionText(host); + const fullAddress = buildFullAddress(host); + const acceptDate = dayjs().format('DD-MMM-YYYY'); + + const agreementVars = { + effectiveDate, + companyName: host.companyName, + companyType: host.companyTypes?.companyTypeName, + fullAddress, + durationText, + expiryDate, + commissionText, + acceptDate, + }; + + const templateBuffer = await getTemplateFromS3(); + const docxBuffer = await generateDocxFromTemplate(templateBuffer, agreementVars); + const pdfBuffer = await generateAgreementPdfBuffer(agreementVars); + + const existingCount = await this.prisma.hostAgreement.count({ + where: { hostXid: host.id, isActive: true }, + }); + + const nextVersionNumber = `AG${existingCount + 1}`; + const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`; + + const docxKey = `${baseKey}.docx`; + const pdfKey = `${baseKey}.pdf`; + + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: docxKey, + Body: docxBuffer, + ContentType: + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ACL: 'private', + }) + .promise(); + + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: pdfKey, + Body: pdfBuffer, + ContentType: 'application/pdf', + ACL: 'private', + }) + .promise(); + + const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`; + + await this.prisma.$transaction(async (tx) => { + // Optional: mark previous agreements inactive + await tx.hostAgreement.updateMany({ + where: { hostXid: host.id, isActive: true }, + data: { isActive: false }, + }); + + await tx.hostAgreement.create({ + data: { + hostXid: host.id, + filePath: pdfUrl, + versionNumber: nextVersionNumber, + isActive: true, + }, + }); + + await tx.hostHeader.update({ + where: { id: host.id }, + data: { + stepper: STEPPER.AGREEMENT_ACCEPTED, + isApproved: true, + agreementAccepted: true, + agreementStartDate: host.agreementStartDate || new Date(), + }, + }); }); }