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

@ -0,0 +1,25 @@
/*
Warnings:
- Added the required column `actorName` to the `Quotation` table without a default value. This is not possible if the table is not empty.
- Added the required column `contactName` to the `Quotation` table without a default value. This is not possible if the table is not empty.
- Added the required column `contactTel` to the `Quotation` table without a default value. This is not possible if the table is not empty.
- Added the required column `documentReceivePoint` to the `Quotation` table without a default value. This is not possible if the table is not empty.
- Added the required column `dueDate` to the `Quotation` table without a default value. This is not possible if the table is not empty.
- Added the required column `workName` to the `Quotation` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Quotation" DROP CONSTRAINT "Quotation_customerId_fkey";
-- AlterTable
ALTER TABLE "Quotation" ADD COLUMN "actorName" TEXT NOT NULL,
ADD COLUMN "contactName" TEXT NOT NULL,
ADD COLUMN "contactTel" TEXT NOT NULL,
ADD COLUMN "documentReceivePoint" TEXT NOT NULL,
ADD COLUMN "dueDate" DATE NOT NULL,
ADD COLUMN "workName" TEXT NOT NULL,
ALTER COLUMN "customerId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1020,16 +1020,21 @@ enum PayCondition {
model Quotation {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
customerBranchId String
customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id])
status Status @default(CREATED)
statusOrder Int @default(0)
code String
code String
actorName String
workName String
contactName String
contactTel String
documentReceivePoint String
dueDate DateTime @db.Date
date DateTime @default(now())
payCondition PayCondition
@ -1051,12 +1056,14 @@ model Quotation {
vatExcluded Float
finalPrice Float
createdAt DateTime @default(now())
createdBy User? @relation(name: "QuotationCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
createdBy User? @relation(name: "QuotationCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @updatedAt
updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedAt DateTime @updatedAt
updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
Customer Customer? @relation(fields: [customerId], references: [id])
customerId String?
}
model QuotationPaySplit {

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");