feat: update quotation endpoints with permission

This commit is contained in:
Methapon Metanipat 2024-09-26 14:32:44 +07:00
parent 3ee1f5ed0f
commit 56c2f1d5ed
3 changed files with 158 additions and 128 deletions

View file

@ -17,10 +17,24 @@ import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import {
branchRelationPermInclude,
createPermCheck,
createPermCondition,
} from "../services/permission";
import { isSystem } from "../utils/keycloak";
import { notFoundError, relationError } from "../utils/error";
type QuotationCreate = {
status?: Status;
actorName: string;
workName: string;
contactName: string;
contactTel: string;
documentReceivePoint: string;
dueDate: Date;
payCondition: PayCondition;
paySplitCount?: number;
@ -47,7 +61,6 @@ type QuotationCreate = {
)[];
customerBranchId: string;
customerId: string;
urgent?: boolean;
@ -79,6 +92,13 @@ type QuotationCreate = {
type QuotationUpdate = {
status?: "ACTIVE" | "INACTIVE";
actorName?: string;
workName?: string;
contactName?: string;
contactTel?: string;
documentReceivePoint?: string;
dueDate?: Date;
payCondition?: PayCondition;
paySplitCount?: number;
@ -106,7 +126,6 @@ type QuotationUpdate = {
)[];
customerBranchId?: string;
customerId?: string;
urgent?: boolean;
@ -142,24 +161,40 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"branch_manager",
"head_of_account",
"account",
"head_of_sale",
"sale",
];
const VAT_DEFAULT = 0.07;
function globalAllow(user: RequestWithUser["user"]) {
const allowList = ["system", "head_of_admin", "admin", "branch_manager", "head_of_account"];
const allowList = ["system", "head_of_admin", "head_of_account", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCheck = createPermCheck(globalAllow);
const permissionCond = createPermCondition(globalAllow);
@Route("/api/v1/quotation")
@Tags("Quotation")
export class QuotationController extends Controller {
@Get()
@Security("keycloak")
async getQuotationList(@Query() page: number = 1, @Query() pageSize: number = 30) {
async getQuotationList(
@Request() req: RequestWithUser,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const [result, total] = await prisma.$transaction([
prisma.quotation.findMany({
where: {
customerBranch: {
customer: {
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
},
},
},
include: {
worker: true,
service: {
@ -168,9 +203,6 @@ export class QuotationController extends Controller {
work: {
include: {
_count: { select: { productOnWork: true } },
productOnWork: {
include: { product: true },
},
},
},
},
@ -206,9 +238,7 @@ export class QuotationController extends Controller {
where: { id: quotationId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
}
if (!record) throw notFoundError("Quotation");
return record;
}
@ -222,12 +252,17 @@ export class QuotationController extends Controller {
a.work.flatMap((b) => b.product.map((c) => c.id)),
);
const [customer, customerBranch, employee, service, product] = await prisma.$transaction([
prisma.customer.findUnique({
where: { id: body.customerId },
}),
const [customerBranch, employee, service, product] = await prisma.$transaction([
prisma.customerBranch.findUnique({
include: { customer: true },
include: {
customer: {
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
},
},
},
where: { id: body.customerBranchId },
}),
prisma.employee.findMany({
@ -242,45 +277,12 @@ export class QuotationController extends Controller {
}),
]);
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",
);
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");
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
const { service: _service, worker: _worker, ...rest } = body;
@ -292,7 +294,7 @@ export class QuotationController extends Controller {
},
create: {
key: `EMPLOYEE_${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
value: 1,
value: nonExistEmployee.length,
},
update: { value: { increment: nonExistEmployee.length } },
});
@ -301,7 +303,7 @@ export class QuotationController extends Controller {
tx.employee.create({
data: {
...v,
code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value + i}`.padStart(7, "0")}`,
code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`,
customerBranchId: customerBranch.id,
},
}),
@ -355,13 +357,13 @@ export class QuotationController extends Controller {
Math.round(
(currentProduct.price * e.amount -
currentProduct.price * e.amount * e.discount) *
(e.vat === undefined ? 0.07 : e.vat) *
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
100,
) / 100;
return {
...e,
vat: e.vat === undefined ? 0.07 : e.vat,
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
pricePerUnit: currentProduct.price,
};
}),
@ -477,13 +479,21 @@ export class QuotationController extends Controller {
@Body() body: QuotationUpdate,
) {
const record = await prisma.quotation.findUnique({
include: { customer: true },
include: {
customerBranch: {
include: {
customer: {
include: {
registeredBranch: { include: branchRelationPermInclude(req.user) },
},
},
},
},
},
where: { id: quotationId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
}
if (!record) throw notFoundError("Quotation");
const existingEmployee = body.worker?.filter((v) => typeof v === "string");
const serviceIdList = body.service?.map((v) => v.id);
@ -491,16 +501,21 @@ export class QuotationController extends Controller {
a.work.flatMap((b) => b.product.map((c) => c.id)),
);
const [customer, customerBranch, employee, service, product] = await prisma.$transaction(
const [customerBranch, employee, service, product] = await prisma.$transaction(
async (tx) =>
await Promise.all([
tx.customer.findFirst({
where: { id: body.customerId },
}),
tx.customerBranch.findFirst({
include: { customer: true },
where: { id: body.customerBranchId },
}),
body.customerBranchId
? tx.customerBranch.findFirst({
include: {
customer: {
include: {
registeredBranch: { include: branchRelationPermInclude(req.user) },
},
},
},
where: { id: body.customerBranchId },
})
: null,
body.worker
? tx.employee.findMany({
where: { id: { in: existingEmployee } },
@ -520,59 +535,32 @@ export class QuotationController extends Controller {
]),
);
if (serviceIdList?.length !== service?.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Some service cannot be found.",
"relationServiceNotFound",
);
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");
await permissionCheck(req.user, record.customerBranch.customer.registeredBranch);
if (customerBranch && record.customerBranchId !== body.customerBranchId) {
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
}
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[] = [];
const branchId = (customerBranch || record.customerBranch).id;
const branchCode = (customerBranch || record.customerBranch).code;
if (body.worker) {
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
const lastEmployee = await tx.runningNo.upsert({
where: {
key: `EMPLOYEE_${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
key: `EMPLOYEE_${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
},
create: {
key: `EMPLOYEE_${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
key: `EMPLOYEE_${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`,
value: 1,
},
update: { value: { increment: nonExistEmployee.length } },
@ -582,8 +570,8 @@ export class QuotationController extends Controller {
tx.employee.create({
data: {
...v,
code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`,
customerBranchId: customerBranch.id,
code: `${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`,
customerBranchId: branchId,
},
}),
),
@ -636,13 +624,13 @@ export class QuotationController extends Controller {
Math.round(
(currentProduct.price * e.amount -
currentProduct.price * e.amount * e.discount) *
(e.vat === undefined ? 0.07 : e.vat) *
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
100,
) / 100;
return {
...e,
vat: e.vat === undefined ? 0.07 : e.vat,
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
pricePerUnit: currentProduct.price,
};
}),
@ -677,7 +665,6 @@ export class QuotationController extends Controller {
data: {
...rest,
statusOrder: +(rest.status === "INACTIVE"),
code: "",
worker:
sortedEmployeeId.length > 0
? {
@ -753,14 +740,25 @@ export class QuotationController extends Controller {
@Delete("{quotationId}")
@Security("keycloak", MANAGE_ROLES)
async deleteQuotationById(@Path() quotationId: string) {
async deleteQuotationById(@Request() req: RequestWithUser, @Path() quotationId: string) {
const record = await prisma.quotation.findUnique({
include: {
customerBranch: {
include: {
customer: {
include: {
registeredBranch: { include: branchRelationPermInclude(req.user) },
},
},
},
},
},
where: { id: quotationId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Quotation not found.", "quotationNotFound");
}
if (!record) throw notFoundError("Quotation");
await permissionCheck(req.user, record.customerBranch.customer.registeredBranch);
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Quotation is in used.", "quotationInUsed");