diff --git a/prisma/migrations/20250127043028_add_product_field/migration.sql b/prisma/migrations/20250127043028_add_product_field/migration.sql new file mode 100644 index 0000000..2cb2cab --- /dev/null +++ b/prisma/migrations/20250127043028_add_product_field/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - Made the column `vatIncluded` on table `Product` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "agentPriceCalcVat" BOOLEAN, +ADD COLUMN "agentPriceVatIncluded" BOOLEAN, +ADD COLUMN "serviceChargeCalcVat" BOOLEAN, +ADD COLUMN "serviceChargeVatIncluded" BOOLEAN, +ALTER COLUMN "vatIncluded" SET NOT NULL, +ALTER COLUMN "vatIncluded" SET DEFAULT true; +UPDATE "Product" SET "agentPriceCalcVat" = "Product"."calcVat" WHERE "agentPriceCalcVat" IS NULL; +UPDATE "Product" SET "agentPriceVatIncluded" = "Product"."vatIncluded" WHERE "agentPriceVatIncluded" IS NULL; +UPDATE "Product" SET "serviceChargeCalcVat" = "Product"."calcVat" WHERE "serviceChargeCalcVat" IS NULL; +UPDATE "Product" SET "serviceChargeVatIncluded" = "Product"."vatIncluded" WHERE "serviceChargeVatIncluded" IS NULL; diff --git a/prisma/migrations/20250127044659_add_default_value_to_new_field/migration.sql b/prisma/migrations/20250127044659_add_default_value_to_new_field/migration.sql new file mode 100644 index 0000000..a547761 --- /dev/null +++ b/prisma/migrations/20250127044659_add_default_value_to_new_field/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Product" ALTER COLUMN "agentPriceCalcVat" SET DEFAULT true, +ALTER COLUMN "agentPriceVatIncluded" SET DEFAULT true, +ALTER COLUMN "serviceChargeCalcVat" SET DEFAULT true, +ALTER COLUMN "serviceChargeVatIncluded" SET DEFAULT true; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2cc73fe..8ad88b5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1103,10 +1103,16 @@ model Product { price Float agentPrice Float serviceCharge Float - vatIncluded Boolean? expenseType String? - calcVat Boolean @default(true) + vatIncluded Boolean @default(true) + calcVat Boolean @default(true) + + agentPriceVatIncluded Boolean? @default(true) + agentPriceCalcVat Boolean? @default(true) + + serviceChargeVatIncluded Boolean? @default(true) + serviceChargeCalcVat Boolean? @default(true) status Status @default(CREATED) statusOrder Int @default(0) diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts index 2ed9a88..b73bd3a 100644 --- a/src/controllers/04-product-controller.ts +++ b/src/controllers/04-product-controller.ts @@ -58,6 +58,10 @@ type ProductCreate = { serviceCharge: number; vatIncluded?: boolean; calcVat?: boolean; + agentPriceVatIncluded?: boolean; + agentPriceCalcVat?: boolean; + serviceChargeVatIncluded?: boolean; + serviceChargeCalcVat?: boolean; expenseType?: string; selectedImage?: string; shared?: boolean; @@ -77,6 +81,10 @@ type ProductUpdate = { remark?: string; vatIncluded?: boolean; calcVat?: boolean; + agentPriceVatIncluded?: boolean; + agentPriceCalcVat?: boolean; + serviceChargeVatIncluded?: boolean; + serviceChargeCalcVat?: boolean; expenseType?: string; selectedImage?: string; shared?: boolean; diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index 4224e45..e653883 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -478,9 +478,11 @@ export class QuotationController extends Controller { const list = body.productServiceList.map((v, i) => { const p = product.find((p) => p.id === v.productId)!; + const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; + const originalPrice = body.agentPrice ? p.agentPrice : p.price; const finalPriceWithVat = precisionRound( - originalPrice + (p.vatIncluded ? 0 : originalPrice * VAT_DEFAULT), + originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), ); const price = finalPriceWithVat; @@ -743,9 +745,11 @@ export class QuotationController extends Controller { const list = body.productServiceList?.map((v, i) => { const p = product.find((p) => p.id === v.productId)!; + const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; + const originalPrice = record.agentPrice ? p.agentPrice : p.price; const finalPriceWithVat = precisionRound( - originalPrice + (p.vatIncluded ? 0 : originalPrice * VAT_DEFAULT), + originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), ); const price = finalPriceWithVat; @@ -919,14 +923,63 @@ export class QuotationActionController extends Controller { @Request() req: RequestWithUser, @Path() quotationId: string, @Body() - body: { - workerId: string; - productServiceId: string[]; - }[], + body: ( + | { + workerId: string; + productServiceId: string[]; + } + | { + workerData: { + dateOfBirth: Date; + gender: string; + nationality: string; + namePrefix?: string; + firstName: string; + firstNameEN: string; + middleName?: string; + middleNameEN?: string; + lastName: string; + lastNameEN: string; + }; + productServiceId: string[]; + } + )[], ) { + const { existsEmployee, newEmployee } = body.reduce<{ + existsEmployee: { + workerId: string; + productServiceId: string[]; + }[]; + newEmployee: { + workerData: { + dateOfBirth: Date; + gender: string; + nationality: string; + namePrefix?: string; + firstName: string; + firstNameEN: string; + middleName?: string; + middleNameEN?: string; + lastName: string; + lastNameEN: string; + }; + productServiceId: string[]; + }[]; + }>( + (acc, current) => { + if ("workerId" in current) { + acc.existsEmployee.push(current); + } else { + acc.newEmployee.push(current); + } + return acc; + }, + { existsEmployee: [], newEmployee: [] }, + ); + const ids = { - employee: body.map((v) => v.workerId), - productService: body + employee: existsEmployee.map((v) => v.workerId), + productService: existsEmployee .flatMap((v) => v.productServiceId) .filter((lhs, i, a) => a.findIndex((rhs) => lhs === rhs) === i), }; @@ -936,6 +989,7 @@ export class QuotationActionController extends Controller { await Promise.all([ tx.quotation.findFirst({ include: { + customerBranch: true, worker: true, _count: { select: { @@ -961,11 +1015,13 @@ export class QuotationActionController extends Controller { if (ids.productService.length !== productService.length) throw relationError("Product"); if ( quotation._count.worker + - body.filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId)) - .length > + existsEmployee.filter( + (lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId), + ).length + + newEmployee.length > (quotation.workerMax || 0) ) { - if (body.length === 0) return; + if (existsEmployee.length === 0) return; throw new HttpError( HttpStatus.PRECONDITION_FAILED, "Worker exceed current quotation max worker.", @@ -974,8 +1030,46 @@ export class QuotationActionController extends Controller { } await prisma.$transaction(async (tx) => { + const customerBranch = quotation.customerBranch; + const lastEmployee = await tx.runningNo.upsert({ + where: { + key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, + }, + create: { + key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, + value: newEmployee.length, + }, + update: { value: { increment: newEmployee.length } }, + }); + + const newEmployeeWithId = await Promise.all( + newEmployee.map(async (v, i) => { + const data = await tx.employee.create({ + data: { + ...v.workerData, + code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`, + customerBranchId: customerBranch.id, + }, + }); + + return { workerId: data.id, productServiceId: v.productServiceId }; + }), + ); + + const rearrange: typeof existsEmployee = []; + + while (body.length > 0) { + const item = body.shift(); + if (item && "workerId" in item) { + rearrange.push(item); + } else { + const popNew = newEmployeeWithId.shift(); + popNew && rearrange.push(popNew); + } + } + await tx.quotationProductServiceWorker.createMany({ - data: body + data: existsEmployee .filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId)) .flatMap((lhs) => lhs.productServiceId.map((rhs) => ({ @@ -1006,7 +1100,7 @@ export class QuotationActionController extends Controller { quotationStatus: QuotationStatus.PaymentSuccess, // NOTE: change back if already complete or canceled worker: { createMany: { - data: body + data: rearrange .filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId)) .map((v, i) => ({ no: quotation._count.worker + i + 1, @@ -1018,7 +1112,7 @@ export class QuotationActionController extends Controller { quotation.quotationStatus === "PaymentInProcess" || quotation.quotationStatus === "PaymentSuccess" ? { - create: body + create: rearrange .filter( (lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId) &&