Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2

This commit is contained in:
2026-02-26 19:53:06 +05:30
2 changed files with 363 additions and 12 deletions

View File

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

View File

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