diff --git a/src/controllers/quotation-controller.ts b/src/controllers/quotation-controller.ts index 90f8a11..ec1893d 100644 --- a/src/controllers/quotation-controller.ts +++ b/src/controllers/quotation-controller.ts @@ -89,7 +89,7 @@ type QuotationCreate = { type QuotationUpdate = { status?: "ACTIVE" | "INACTIVE"; - payCondition: PayCondition; + payCondition?: PayCondition; paySplitCount?: number; paySplit?: Date[]; @@ -124,16 +124,17 @@ type QuotationUpdate = { } )[]; - customerBranchId: string; - customerId: string; + customerBranchId?: string; + customerId?: string; urgent?: boolean; - service: { + service?: { id: string; // Other fields will come from original data work: { id: string; + excluded?: boolean; // Name field will come from original data product: { id: string; @@ -485,12 +486,295 @@ export class QuotationController extends Controller { @Body() body: QuotationUpdate, ) { const record = await prisma.quotation.findUnique({ + include: { customer: true }, where: { id: quotationId }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound"); } + + const existingEmployee = body.worker?.filter((v) => typeof v === "string"); + const serviceIdList = body.service?.map((v) => v.id); + const productIdList = body.service?.flatMap((a) => + a.work.flatMap((b) => b.product.map((c) => c.id)), + ); + + const [customer, customerBranch, employee, service, product] = await prisma.$transaction( + async (tx) => + await Promise.all([ + tx.customer.findUnique({ + where: { id: body.customerId }, + }), + tx.customerBranch.findUnique({ + include: { customer: true }, + where: { id: body.customerBranchId }, + }), + body.worker + ? tx.employee.findMany({ + where: { id: { in: existingEmployee } }, + }) + : null, + body.service + ? tx.service.findMany({ + include: { work: true }, + where: { id: { in: serviceIdList } }, + }) + : null, + body.service + ? tx.product.findMany({ + where: { id: { in: productIdList } }, + }) + : null, + ]), + ); + + if (serviceIdList?.length !== service?.length) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Some service cannot be found.", + "relationServiceNotFound", + ); + } + if (productIdList?.length !== product?.length) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Some product cannot be found.", + "relationProductNotFound", + ); + } + if (existingEmployee?.length !== employee?.length) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Some worker(employee) cannot be found.", + "relationWorkerNotFound", + ); + } + if (!customer) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer cannot be found.", + "relationCustomerNotFound", + ); + if (!customerBranch) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer Branch cannot be found.", + "relationCustomerBranchNotFound", + ); + if (customerBranch.customerId !== customer.id) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer conflict with customer branch.", + "customerConflictCustomerBranch", + ); + + const { service: _service, worker: _worker, ...rest } = body; + + return await prisma.$transaction(async (tx) => { + const sortedEmployeeId: string[] = []; + + if (body.worker) { + const nonExistEmployee = body.worker.filter((v) => typeof v !== "string"); + const lastEmployee = await tx.runningNo.upsert({ + where: { + key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`, + }, + create: { + key: `EMPLOYEE_${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}`, + value: 1, + }, + update: { value: { increment: nonExistEmployee.length } }, + }); + const newEmployee = await Promise.all( + nonExistEmployee.map(async (v, i) => + tx.employee.create({ + data: { + ...v, + code: `${customerBranch.customer.code}-${customerBranch.branchNo.toString().padStart(2, "0")}-${new Date().getFullYear().toString().slice(-2).padStart(2, "0")}${(lastEmployee.value + i).toString().padStart(4, "0")}`, + customerBranchId: customerBranch.id, + }, + }), + ), + ); + + while (body.worker.length > 0) { + const popExist = body.worker.shift(); + if (typeof popExist === "string") sortedEmployeeId.push(popExist); + else { + const popNew = newEmployee.shift(); + popNew && sortedEmployeeId.push(popNew.id); + } + } + } + + const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 }; + + const restructureService = body.service?.flatMap((a) => { + const currentService = service?.find((b) => b.id === a.id); + + if (!currentService) return []; // should not possible + + return { + id: currentService.id, + name: currentService.name, + code: currentService.code, + detail: currentService.detail, + attributes: currentService.attributes as Prisma.JsonObject, + work: a.work.flatMap((c) => { + if (c.excluded) return []; + + const currentWork = currentService.work.find((d) => d.id === c.id); + + if (!currentWork) return []; // additional will get stripped + + return { + id: currentWork.id, + order: currentWork.order, + name: currentWork.name, + attributes: currentWork.attributes as Prisma.JsonObject, + product: c.product.flatMap((e) => { + const currentProduct = product?.find((f) => f.id === e.id); + + if (!currentProduct) return []; // should not possible + + price.totalPrice += currentProduct.price; + price.totalDiscount += Math.round(currentProduct.price * e.discount * 100) / 100; + price.totalVat += + Math.round( + (currentProduct.price - currentProduct.price * e.discount) * + (e.vat === undefined ? 0.07 : e.vat) * + 100, + ) / 100; + + return { + ...e, + vat: e.vat === undefined ? 0.07 : e.vat, + pricePerUnit: currentProduct.price, + }; + }), + }; + }), + }; + }); + + const quotation = await tx.quotation.update({ + include: { + worker: { + include: { employee: true }, + }, + }, + where: { id: quotationId }, + data: { + ...rest, + statusOrder: +(rest.status === "INACTIVE"), + code: "", + worker: + sortedEmployeeId.length > 0 + ? { + deleteMany: { id: { notIn: sortedEmployeeId } }, + createMany: { + skipDuplicates: true, + data: sortedEmployeeId.map((v, i) => ({ + no: i, + code: "", + employeeId: v, + })), + }, + } + : undefined, + totalPrice: body.service ? price.totalPrice : undefined, + totalDiscount: body.service ? price.totalDiscount : undefined, + + vat: body.service ? price.totalVat : undefined, + vatExcluded: body.service ? 0 : undefined, + + finalPrice: body.service ? price.totalPrice - price.totalDiscount : undefined, + + paySplit: rest.paySplit + ? { + deleteMany: {}, + createMany: { + data: (rest.paySplit || []).map((v, i) => ({ + no: i + 1, + date: v, + })), + }, + } + : undefined, + + service: body.service ? { deleteMany: {} } : undefined, + + updatedByUserId: req.user.sub, + }, + }); + + if (restructureService) + await Promise.all( + restructureService.map(async (a) => { + const { id: _currentServiceId } = await tx.quotationService.create({ + data: { + code: a.code, + name: a.name, + detail: a.detail, + attributes: a.attributes, + quotationId: quotation.id, + refServiceId: a.id, + }, + }); + + await Promise.all( + a.work.map(async (b) => { + await tx.quotationServiceWork.create({ + data: { + order: b.order, + name: b.name, + attributes: b.attributes, + serviceId: _currentServiceId, + productOnWork: { + createMany: { + data: b.product.map((v, i) => ({ + productId: v.id, + order: i + 1, + vat: v.vat, + amount: v.amount, + discount: v.discount, + pricePerUnit: v.pricePerUnit, + })), + }, + }, + }, + }); + }), + ); + }), + ); + + return await tx.quotation.findUnique({ + include: { + service: { + include: { + work: { + include: { + productOnWork: { + include: { product: true }, + }, + }, + }, + }, + }, + paySplit: true, + worker: true, + customerBranch: { + include: { customer: true }, + }, + _count: { + select: { service: true }, + }, + }, + where: { id: quotation.id }, + }); + }); } @Delete("{quotationId}")