refactor: update quotation structure and endpoint

This commit is contained in:
Methapon Metanipat 2024-10-01 15:36:45 +07:00
parent 4f4b8df9a3
commit 71ffd895f6
3 changed files with 262 additions and 363 deletions

View file

@ -64,28 +64,22 @@ type QuotationCreate = {
urgent?: boolean;
service: {
id: string;
// Other fields will come from original data
work: {
id: string;
// Name field will come from original data
excluded?: boolean;
product: {
id: string;
amount: number;
/**
* @maximum 1
* @minimum 0
*/
discount: number;
/**
* @maximum 1
* @minimum 0
*/
vat?: number;
}[];
}[];
productServiceList: {
serviceId?: string;
workId?: string;
productId: string;
amount: number;
/**
* @maximum 1
* @minimum 0
*/
discount: number;
pricePerUnit?: number;
/**
* @maximum 1
* @minimum 0
*/
vat?: number;
}[];
};
@ -129,31 +123,22 @@ type QuotationUpdate = {
urgent?: boolean;
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;
/**
* @isInt
*/
amount: number;
/**
* @maximum 1
* @minimum 0
*/
discount: number;
/**
* @maximum 1
* @minimum 0
*/
vat: number;
}[];
}[];
productServiceList?: {
serviceId?: string;
workId?: string;
productId: string;
amount: number;
/**
* @maximum 1
* @minimum 0
*/
discount: number;
pricePerUnit?: number;
/**
* @maximum 1
* @minimum 0
*/
vat?: number;
}[];
};
@ -202,14 +187,11 @@ export class QuotationController extends Controller {
include: {
customerBranch: true,
worker: true,
service: {
productServiceList: {
include: {
_count: { select: { work: true } },
work: {
include: {
_count: { select: { productOnWork: true } },
},
},
product: true,
work: true,
service: true,
},
},
},
@ -231,17 +213,11 @@ export class QuotationController extends Controller {
worker: {
include: { employee: true },
},
service: {
productServiceList: {
include: {
_count: { select: { work: true } },
work: {
include: {
_count: { select: { productOnWork: true } },
productOnWork: {
include: { product: true },
},
},
},
product: true,
work: true,
service: true,
},
},
},
@ -256,45 +232,49 @@ export class QuotationController extends Controller {
@Post()
@Security("keycloak", MANAGE_ROLES)
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
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 ids = {
employee: body.worker.filter((v) => typeof v === "string"),
product: body.productServiceList
.map((v) => v.productId)
.filter((v, i, a) => a.findIndex((c) => c === v) === i),
work: body.productServiceList.map((v) => v.workId || []).flat(),
service: body.productServiceList.map((v) => v.serviceId || []).flat(),
};
const [customerBranch, employee, service, product] = await prisma.$transaction([
prisma.customerBranch.findUnique({
include: {
customer: {
const [customerBranch, employee, product, work, service] = await prisma.$transaction(
async (tx) =>
await Promise.all([
tx.customerBranch.findUnique({
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
customer: {
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
},
},
},
},
where: { id: body.customerBranchId },
}),
prisma.employee.findMany({
where: { id: { in: existingEmployee } },
}),
prisma.service.findMany({
include: { work: true },
where: { id: { in: serviceIdList } },
}),
prisma.product.findMany({
where: { id: { in: productIdList } },
}),
]);
where: { id: body.customerBranchId },
}),
tx.employee.findMany({ where: { id: { in: ids.employee } } }),
tx.product.findMany({ where: { id: { in: ids.product } } }),
ids.work.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null,
ids.service.length ? tx.service.findMany({ where: { id: { in: ids.service } } }) : null,
]),
);
if (serviceIdList.length !== service.length) throw relationError("Service");
if (productIdList.length !== product.length) throw relationError("Product");
if (existingEmployee.length !== employee.length) throw relationError("Worker");
if (!customerBranch) throw relationError("Customer Branch");
if (ids.employee.length !== employee.length) throw relationError("Worker");
if (ids.product.length !== product.length) throw relationError("Product");
if (ids.work.length && ids.work.length !== work?.length) throw relationError("Work");
if (ids.service.length && ids.service.length !== service?.length) {
throw relationError("Service");
}
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
const { service: _service, worker: _worker, ...rest } = body;
const { productServiceList: _productServiceList, worker: _worker, ...rest } = body;
return await prisma.$transaction(async (tx) => {
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
@ -330,58 +310,6 @@ export class QuotationController extends Controller {
}
}
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 * e.amount;
price.totalDiscount +=
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
price.totalVat +=
Math.round(
(currentProduct.price * e.amount -
currentProduct.price * e.amount * e.discount) *
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
100,
) / 100;
return {
...e,
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
pricePerUnit: currentProduct.price,
};
}),
};
}),
};
});
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
const currentDate = new Date().getDate();
@ -397,17 +325,46 @@ export class QuotationController extends Controller {
update: { value: { increment: 1 } },
});
const list = body.productServiceList.map((v, i) => ({
order: i + 1,
productId: v.productId,
workId: v.workId,
serviceId: v.serviceId,
pricePerUnit: v.pricePerUnit || product.find((p) => p.id === v.productId)?.price || 0,
amount: v.amount,
discount: v.discount,
vat: v.vat || VAT_DEFAULT,
}));
const price = list.reduce(
(a, c) => {
const price = c.pricePerUnit * c.amount;
const discount = price * c.discount;
const vat = (price - discount) * c.vat;
a.totalPrice += price;
a.totalDiscount += discount;
a.vat += vat;
a.finalPrice += price - discount + vat;
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
return await tx.quotation.create({
include: {
service: {
productServiceList: {
include: {
work: {
include: {
productOnWork: {
include: { product: true },
},
},
},
product: true,
work: true,
service: true,
},
},
paySplit: true,
@ -416,12 +373,13 @@ export class QuotationController extends Controller {
include: { customer: true },
},
_count: {
select: { service: true },
select: { productServiceList: true },
},
},
data: {
...rest,
...price,
statusOrder: +(rest.status === "INACTIVE"),
code: `${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(4, "0")}`,
worker: {
@ -433,11 +391,6 @@ export class QuotationController extends Controller {
})),
},
},
totalPrice: price.totalPrice,
totalDiscount: price.totalDiscount,
vat: price.totalVat,
vatExcluded: 0,
finalPrice: price.totalPrice - price.totalDiscount,
paySplit: {
createMany: {
data: (rest.paySplit || []).map((v, i) => ({
@ -446,34 +399,7 @@ export class QuotationController extends Controller {
})),
},
},
service: {
create: restructureService.map((a) => ({
code: a.code,
name: a.name,
detail: a.detail,
attributes: a.attributes,
refServiceId: a.id,
work: {
create: a.work.map((b) => ({
order: b.order,
name: b.name,
attributes: b.attributes,
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,
})),
},
},
})),
},
})),
},
productServiceList: { create: list },
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
@ -505,57 +431,53 @@ export class QuotationController extends Controller {
if (!record) throw notFoundError("Quotation");
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 ids = {
employee: body.worker?.filter((v) => typeof v === "string"),
product: body.productServiceList
?.map((v) => v.productId)
.filter((v, i, a) => a.findIndex((c) => c === v) === i),
work: body.productServiceList?.map((v) => v.workId || []).flat(),
service: body.productServiceList?.map((v) => v.serviceId || []).flat(),
};
const [customerBranch, employee, service, product] = await prisma.$transaction(
const [customerBranch, employee, product, work, service] = await prisma.$transaction(
async (tx) =>
await Promise.all([
body.customerBranchId
? tx.customerBranch.findFirst({
tx.customerBranch.findUnique({
include: {
customer: {
include: {
customer: {
include: {
registeredBranch: { include: branchRelationPermInclude(req.user) },
},
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
where: { id: body.customerBranchId },
})
: null,
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,
},
},
where: { id: body.customerBranchId },
}),
tx.employee.findMany({ where: { id: { in: ids.employee } } }),
tx.product.findMany({ where: { id: { in: ids.product } } }),
ids.work?.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null,
ids.service?.length ? tx.service.findMany({ where: { id: { in: ids.service } } }) : null,
]),
);
if (serviceIdList?.length !== service?.length) throw relationError("Service");
if (productIdList?.length !== product?.length) throw relationError("Product");
if (existingEmployee?.length !== employee?.length) throw relationError("Worker");
if (body.customerBranchId && !customerBranch) throw relationError("Customer Branch");
if (ids.employee && ids.employee.length !== employee.length) throw relationError("Worker");
if (ids.product && ids.product.length !== product.length) throw relationError("Product");
if (ids.work && ids.work.length && ids.work.length !== work?.length) {
throw relationError("Work");
}
if (ids.service && ids.service.length && ids.service.length !== service?.length) {
throw relationError("Service");
}
await permissionCheck(req.user, record.customerBranch.customer.registeredBranch);
if (customerBranch && record.customerBranchId !== body.customerBranchId) {
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
}
const { service: _service, worker: _worker, ...rest } = body;
const { productServiceList: _productServiceList, worker: _worker, ...rest } = body;
return await prisma.$transaction(async (tx) => {
const sortedEmployeeId: string[] = [];
@ -597,69 +519,46 @@ export class QuotationController extends Controller {
}
}
const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 };
const list = body.productServiceList?.map((v, i) => ({
order: i + 1,
productId: v.productId,
workId: v.workId,
serviceId: v.serviceId,
pricePerUnit: v.pricePerUnit || product.find((p) => p.id === v.productId)?.price || 0,
amount: v.amount,
discount: v.discount,
vat: v.vat || VAT_DEFAULT,
}));
const restructureService = body.service?.flatMap((a) => {
const currentService = service?.find((b) => b.id === a.id);
const price = list?.reduce(
(a, c) => {
const price = c.pricePerUnit * c.amount;
const discount = price * c.discount;
const vat = price - discount * c.vat;
if (!currentService) return []; // should not possible
a.totalPrice += price;
a.totalDiscount += discount;
a.vat += vat;
a.finalPrice += price - discount + vat;
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 * e.amount;
price.totalDiscount +=
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
price.totalVat +=
Math.round(
(currentProduct.price * e.amount -
currentProduct.price * e.amount * e.discount) *
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
100,
) / 100;
return {
...e,
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
pricePerUnit: currentProduct.price,
};
}),
};
}),
};
});
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
return await tx.quotation.update({
include: {
service: {
productServiceList: {
include: {
work: {
include: {
productOnWork: {
include: { product: true },
},
},
},
product: true,
work: true,
service: true,
},
},
paySplit: true,
@ -668,12 +567,13 @@ export class QuotationController extends Controller {
include: { customer: true },
},
_count: {
select: { service: true },
select: { productServiceList: true },
},
},
where: { id: quotationId },
data: {
...rest,
...price,
statusOrder: +(rest.status === "INACTIVE"),
worker:
sortedEmployeeId.length > 0
@ -689,14 +589,6 @@ export class QuotationController extends Controller {
},
}
: 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: {},
@ -708,40 +600,10 @@ export class QuotationController extends Controller {
},
}
: undefined,
service:
body.service && restructureService
? {
deleteMany: {},
create: restructureService.map((a) => ({
code: a.code,
name: a.name,
detail: a.detail,
attributes: a.attributes,
refServiceId: a.id,
work: {
create: a.work.map((b) => ({
order: b.order,
name: b.name,
attributes: b.attributes,
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,
})),
},
},
})),
},
})),
}
: undefined,
productServiceList: {
deleteMany: {},
create: list,
},
updatedByUserId: req.user.sub,
},
});