Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-01-10 16:53:34 +07:00
commit f1d4584d02
17 changed files with 569 additions and 76 deletions

View file

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "CreditNoteStatus" AS ENUM ('Pending', 'Success');
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "creditNoteStatus" "CreditNoteStatus";

View file

@ -0,0 +1,9 @@
/*
Warnings:
- Made the column `creditNoteStatus` on table `CreditNote` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "CreditNote" ALTER COLUMN "creditNoteStatus" SET NOT NULL,
ALTER COLUMN "creditNoteStatus" SET DEFAULT 'Pending';

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "value" DOUBLE PRECISION NOT NULL DEFAULT 0;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `code` to the `CreditNote` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "code" TEXT NOT NULL;

View file

@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "createdByUserId" TEXT;
-- AddForeignKey
ALTER TABLE "CreditNote" ADD CONSTRAINT "CreditNote_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "TaskStatus" ADD VALUE 'Restart';

View file

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "UserTaskStatus" ADD VALUE 'Restart';

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "UserTask" ADD COLUMN "acceptedAt" TIMESTAMP(3),
ADD COLUMN "submittedAt" TIMESTAMP(3);

View file

@ -0,0 +1,10 @@
-- CreateEnum
CREATE TYPE "CreditNotePaybackType" AS ENUM ('Cash', 'BankTransfer');
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "detail" TEXT,
ADD COLUMN "paybackAccount" TEXT,
ADD COLUMN "paybackAccountName" TEXT,
ADD COLUMN "paybackBank" TEXT,
ADD COLUMN "paybackType" "CreditNotePaybackType",
ADD COLUMN "reason" TEXT;

View file

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "PaybackStatus" AS ENUM ('Pending', 'Verify', 'Done');
-- AlterTable
ALTER TABLE "CreditNote" ADD COLUMN "paybackStatus" "PaybackStatus" NOT NULL DEFAULT 'Pending';

View file

@ -482,6 +482,7 @@ model User {
notificationReceive Notification[] @relation("NotificationReceiver")
notificationRead Notification[]
taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser")
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
requestWorkStepStatus RequestWorkStepStatus[]
userTask UserTask[]
@ -1497,6 +1498,7 @@ enum TaskStatus {
Success
Failed
Restart
Redo
Validate
@ -1559,6 +1561,7 @@ model TaskOrder {
enum UserTaskStatus {
Pending // Should not be use but define here for type
Restart
Accept
Submit
}
@ -1572,16 +1575,53 @@ model UserTask {
taskOrderId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
acceptedAt DateTime?
submittedAt DateTime?
}
enum CreditNoteStatus {
Pending
Success
}
enum CreditNotePaybackType {
Cash
BankTransfer
}
enum PaybackStatus {
Pending
Verify
Done
}
model CreditNote {
id String @id @default(cuid())
code String
creditNoteStatus CreditNoteStatus @default(Pending)
value Float @default(0)
reason String?
detail String?
paybackType CreditNotePaybackType?
paybackBank String?
paybackAccount String?
paybackAccountName String?
paybackStatus PaybackStatus @default(Pending)
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
quotationId String
// NOTE: only status cancel
requestWork RequestWork[]
createdAt DateTime @default(now())
createdBy User? @relation(name: "CreditNoteCreatedByUser", fields: [createdByUserId], references: [id])
createdByUserId String?
}
model DebitNote {

View file

@ -642,11 +642,13 @@ export class UserController extends Controller {
}
}
if (body.username) {
if (body.username || body.email || body.firstName || body.lastName) {
await editUser(userId, {
firstName: body.firstName,
lastName: body.lastName,
username: body.username,
email: body.email,
enabled: body.status !== "INACTIVE",
enabled: body.status ? body.status !== "INACTIVE" : undefined,
});
} else if (body.status) {
await editUser(userId, { enabled: body.status !== "INACTIVE" });

View file

@ -29,6 +29,7 @@ import { queryOrNot } from "../utils/relation";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWorkStatus } from "../generated/kysely/types";
type QuotationCreate = {
registeredBranchId: string;
@ -190,6 +191,8 @@ export class QuotationController extends Controller {
@Query() payCondition?: PayCondition,
@Query() status?: QuotationStatus,
@Query() urgentFirst?: boolean,
@Query() includeRegisteredBranch?: boolean,
@Query() hasCancel?: boolean,
@Query() code?: string,
@Query() query = "",
) {
@ -214,6 +217,18 @@ export class QuotationController extends Controller {
payCondition,
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
quotationStatus: status,
requestData: hasCancel
? {
some: {
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
},
}
: undefined,
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -223,6 +238,7 @@ export class QuotationController extends Controller {
_count: {
select: { worker: true },
},
registeredBranch: includeRegisteredBranch,
customerBranch: {
include: {
customer: {
@ -246,6 +262,46 @@ export class QuotationController extends Controller {
prisma.quotation.count({ where }),
]);
if (hasCancel) {
const canceled = await prisma.requestData.findMany({
include: {
_count: {
select: {
requestWork: {
where: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
},
},
},
where: {
requestWork: {
some: {
creditNoteId: null,
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
},
},
quotationId: { in: result.map((v) => v.id) },
},
});
return {
result: result.map((v) => {
const canceledCount = canceled
.filter((item) => item.quotationId === v.id)
.reduce((a, c) => a + c._count.requestWork, 0);
return Object.assign(v, {
_count: { ...v._count, canceledWork: canceledCount },
});
}),
page,
pageSize,
total,
};
}
return { result: result, page, pageSize, total };
}
@ -419,11 +475,11 @@ export class QuotationController extends Controller {
const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
const price = body.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? precisionRound(price / (1 + VAT_DEFAULT)) : price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? precisionRound(
(pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) * VAT_DEFAULT,
) * (!v.discount ? v.amount : 1)
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
@ -684,11 +740,11 @@ export class QuotationController extends Controller {
const list = body.productServiceList?.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
const price = record.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? precisionRound(price / (1 + VAT_DEFAULT)) : price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? precisionRound(
(pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) * VAT_DEFAULT,
) * (!v.discount ? v.amount : 1)
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
order: i + 1,
@ -980,7 +1036,7 @@ export class QuotationActionController extends Controller {
@Route("api/v1/quotation/{quotationId}/attachment")
@Tags("Quotation")
export class QuotationFileController extends Controller {
private async checkPermission(user: RequestWithUser["user"], id: string) {
async #checkPermission(user: RequestWithUser["user"], id: string) {
const data = await prisma.quotation.findUnique({
include: {
registeredBranch: {
@ -989,14 +1045,14 @@ export class QuotationFileController extends Controller {
},
where: { id },
});
if (!data) throw notFoundError("Payment");
if (!data) throw notFoundError("Quotation");
await permissionCheck(user, data.registeredBranch);
}
@Get()
@Security("keycloak")
async listAttachment(@Request() req: RequestWithUser, @Path() quotationId: string) {
await this.checkPermission(req.user, quotationId);
await this.#checkPermission(req.user, quotationId);
return await listFile(fileLocation.quotation.attachment(quotationId));
}
@ -1018,7 +1074,7 @@ export class QuotationFileController extends Controller {
@Path() quotationId: string,
@Path() name: string,
) {
await this.checkPermission(req.user, quotationId);
await this.#checkPermission(req.user, quotationId);
return await getFile(fileLocation.quotation.attachment(quotationId, name));
}
@ -1029,7 +1085,7 @@ export class QuotationFileController extends Controller {
@Path() quotationId: string,
@Path() name: string,
) {
await this.checkPermission(req.user, quotationId);
await this.#checkPermission(req.user, quotationId);
return await setFile(fileLocation.quotation.attachment(quotationId, name));
}
@ -1040,7 +1096,7 @@ export class QuotationFileController extends Controller {
@Path() quotationId: string,
@Path() name: string,
) {
await this.checkPermission(req.user, quotationId);
await this.#checkPermission(req.user, quotationId);
return await deleteFile(fileLocation.quotation.attachment(quotationId, name));
}
}

View file

@ -292,6 +292,8 @@ export class RequestListController extends Controller {
@Query() requestDataId?: string,
@Query() workStatus?: RequestWorkStatus,
@Query() readyToTask?: boolean,
@Query() cancelOnly?: boolean,
@Query() quotationId?: string,
) {
let statusCondition: Prisma.RequestWorkWhereInput["stepStatus"] = {};
@ -313,16 +315,29 @@ export class RequestListController extends Controller {
],
},
};
} else {
}
if (cancelOnly) {
statusCondition = {
some: { workStatus: RequestWorkStatus.Canceled },
};
}
if (workStatus && !readyToTask && !cancelOnly) {
statusCondition = {
some: { workStatus },
};
}
const where = {
stepStatus: readyToTask || workStatus ? statusCondition : undefined,
stepStatus: readyToTask || cancelOnly || workStatus ? statusCondition : undefined,
creditNote: cancelOnly ? null : undefined,
request: {
id: requestDataId,
requestDataStatus: readyToTask
? { notIn: [RequestDataStatus.Canceled, RequestDataStatus.Completed] }
: undefined,
quotationId,
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
},

View file

@ -125,6 +125,7 @@ export class TaskController extends Controller {
userTask: true,
taskList: true,
institution: true,
registeredBranch: true,
createdBy: true,
},
}),
@ -150,6 +151,7 @@ export class TaskController extends Controller {
where: {
requestWorkStep: { responsibleUserId: taskAssignedUserId },
},
orderBy: { id: "asc" },
include: {
requestWorkStep: {
include: {
@ -451,6 +453,7 @@ export class TaskActionController extends Controller {
return await prisma.$transaction(async (tx) => {
const promises = body.map(async (v) => {
const record = await tx.task.findFirst({
include: { requestWorkStep: true },
where: {
step: v.step,
requestWorkId: v.requestWorkId,
@ -458,6 +461,16 @@ export class TaskActionController extends Controller {
},
});
if (!record) throw notFoundError("Task List");
if (v.taskStatus === TaskStatus.Restart && record.requestWorkStep.responsibleUserId) {
await tx.userTask.updateMany({
where: {
taskOrderId: record.taskOrderId,
userId: record.requestWorkStep.responsibleUserId,
},
data: { userTaskStatus: UserTaskStatus.Restart },
});
}
return await tx.task.update({
where: { id: record.id },
data: {
@ -512,20 +525,14 @@ export class TaskActionController extends Controller {
taskOrderId: taskOrderId,
userId: submitUserId,
},
data: { userTaskStatus: UserTaskStatus.Submit },
data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() },
}),
]);
}
@Post("complete")
@Security("keycloak")
async completeTaskOrder(
@Request() req: RequestWithUser,
@Path() taskOrderId: string,
@Query() submitUserId?: string,
) {
submitUserId = submitUserId ?? req.user.sub;
async completeTaskOrder(@Request() req: RequestWithUser, @Path() taskOrderId: string) {
const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } });
if (!record) throw notFoundError("Task Order");
@ -534,21 +541,54 @@ export class TaskActionController extends Controller {
await Promise.all([
tx.taskOrder.update({
where: { id: taskOrderId },
data: { taskOrderStatus: TaskOrderStatus.Complete },
data: {
taskOrderStatus: TaskOrderStatus.Complete,
userTask: {
updateMany: {
where: { taskOrderId },
data: {
userTaskStatus: UserTaskStatus.Submit,
},
},
},
},
}),
tx.requestWorkStepStatus.updateMany({
where: {
task: {
some: { taskOrderId, taskStatus: TaskStatus.Redo },
some: {
taskOrderId,
taskStatus: {
notIn: [
TaskStatus.Canceled,
TaskStatus.Success,
TaskStatus.Validate,
TaskStatus.Complete,
],
},
},
},
},
data: { workStatus: RequestWorkStatus.Ready },
}),
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: {
notIn: [
TaskStatus.Canceled,
TaskStatus.Success,
TaskStatus.Validate,
TaskStatus.Complete,
],
},
},
data: { taskStatus: TaskStatus.Redo },
}),
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: TaskStatus.Validate,
requestWorkStep: { responsibleUserId: submitUserId },
},
data: { taskStatus: TaskStatus.Complete },
}),
@ -708,17 +748,38 @@ export class UserTaskController extends Controller {
requestWorkStep: { responsibleUserId: req.user.sub },
},
},
userTask: userTaskStatus
? {
some:
userTaskStatus !== UserTaskStatus.Pending
? {
userTaskStatus,
userId: req.user.sub,
}
: undefined,
none: userTaskStatus === UserTaskStatus.Pending ? { userId: req.user.sub } : undefined,
}
AND: userTaskStatus
? [
{
OR:
userTaskStatus === UserTaskStatus.Pending
? [
{
userTask: {
some: {
userTaskStatus: {
in: [UserTaskStatus.Pending, UserTaskStatus.Restart],
},
userId: req.user.sub,
},
},
},
{
userTask: { none: { userId: req.user.sub } },
},
]
: undefined,
userTask:
userTaskStatus !== UserTaskStatus.Pending
? {
some: {
userTaskStatus,
userId: req.user.sub,
},
}
: undefined,
},
]
: undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
@ -735,6 +796,7 @@ export class UserTaskController extends Controller {
userTask: {
where: { userId: req.user.sub },
},
registeredBranch: true,
institution: true,
createdBy: true,
},
@ -783,9 +845,11 @@ export class UserTaskController extends Controller {
data: {
taskOrderStatus: TaskOrderStatus.InProgress,
userTask: {
deleteMany: { userId: req.user.sub },
create: {
userId: req.user.sub,
userTaskStatus: UserTaskStatus.Accept,
acceptedAt: new Date(),
},
},
},
@ -793,7 +857,7 @@ export class UserTaskController extends Controller {
tx.task.updateMany({
where: {
taskOrderId: taskOrderId,
taskStatus: TaskStatus.Pending,
taskStatus: { in: [TaskStatus.Pending, TaskStatus.Restart] },
requestWorkStep: { responsibleUserId: req.user.sub },
},
data: {
@ -812,13 +876,6 @@ export class UserTaskController extends Controller {
},
data: { requestDataStatus: RequestDataStatus.InProgress },
}),
tx.userTask.create({
data: {
userId: req.user.sub,
taskOrderId: taskOrderId,
userTaskStatus: UserTaskStatus.Accept,
},
}),
]);
await Promise.all(promises);

View file

@ -3,6 +3,7 @@ import {
Controller,
Delete,
Get,
Head,
Path,
Post,
Put,
@ -13,7 +14,6 @@ import {
Tags,
} from "tsoa";
// import { Prisma } from "@prisma/client";
import prisma from "../db";
import { RequestWithUser } from "../interfaces/user";
@ -24,10 +24,11 @@ import {
} from "../services/permission";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import { notFoundError } from "../utils/error";
import { Prisma } from "@prisma/client";
import { CreditNotePaybackType, CreditNoteStatus, Prisma } from "@prisma/client";
import { queryOrNot } from "../utils/relation";
import { RequestWorkStatus } from "../generated/kysely/types";
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
const MANAGE_ROLES = [
"system",
@ -53,10 +54,22 @@ const permissionCheckCompany = createPermCheck((_) => true);
type CreditNoteCreate = {
requestWorkId: string[];
quotationId: string;
reason?: string;
detail?: string;
paybackType?: CreditNotePaybackType;
paybackBank?: string;
paybackAccount?: string;
paybackAccountName?: string;
};
type CreditNoteUpdate = {
requestWorkId: string[];
quotationId: string;
reason?: string;
detail?: string;
paybackType?: CreditNotePaybackType;
paybackBank?: string;
paybackAccount?: string;
paybackAccountName?: string;
};
@Route("api/v1/credit-note")
@ -64,7 +77,7 @@ type CreditNoteUpdate = {
export class CreditNoteController extends Controller {
@Get("stats")
@Security("keycloak")
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId: string) {
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
const where = {
requestWork: {
some: {
@ -88,8 +101,16 @@ export class CreditNoteController extends Controller {
@Query() pageSize: number = 30,
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
) {
return await this.getCreditNoteListByCriteria(req, page, pageSize, query, quotationId);
return await this.getCreditNoteListByCriteria(
req,
page,
pageSize,
query,
quotationId,
creditNoteStatus,
);
}
// NOTE: only when needed or else remove this and implement in getCreditNoteList
@ -101,6 +122,7 @@ export class CreditNoteController extends Controller {
@Query() pageSize: number = 30,
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
@Body() body?: {},
) {
const where = {
@ -146,6 +168,7 @@ export class CreditNoteController extends Controller {
},
},
]),
creditNoteStatus,
requestWork: {
some: {
request: {
@ -162,6 +185,16 @@ export class CreditNoteController extends Controller {
prisma.creditNote.findMany({
where,
include: {
quotation: {
include: {
registeredBranch: true,
customerBranch: {
include: {
customer: true,
},
},
},
},
requestWork: {
include: { request: true },
},
@ -191,8 +224,33 @@ export class CreditNoteController extends Controller {
return prisma.creditNote.findFirst({
where,
include: {
quotation: {
include: {
registeredBranch: true,
customerBranch: {
include: {
customer: true,
},
},
},
},
requestWork: {
include: { request: true },
include: {
request: {
include: { employee: true },
},
productService: {
include: {
service: true,
work: {
include: { productOnWork: true },
},
product: {
include: { document: true },
},
},
},
},
},
},
});
@ -203,6 +261,7 @@ export class CreditNoteController extends Controller {
async createCreditNote(@Request() req: RequestWithUser, @Body() body: CreditNoteCreate) {
const requestWork = await prisma.requestWork.findMany({
where: {
creditNote: null,
request: {
quotation: {
id: body.quotationId,
@ -216,6 +275,15 @@ export class CreditNoteController extends Controller {
id: { in: body.requestWorkId },
},
include: {
stepStatus: true,
productService: {
include: {
product: true,
work: {
include: { productOnWork: true },
},
},
},
request: {
include: {
quotation: {
@ -236,28 +304,74 @@ export class CreditNoteController extends Controller {
requestWork.map((item) => permissionCheck(req.user, item.request.quotation.registeredBranch)),
);
const record = await prisma.creditNote.create({
include: {
requestWork: {
include: {
request: true,
},
},
quotation: true,
},
data: {
requestWork: {
connect: body.requestWorkId.map((v) => ({
id: v,
})),
},
quotationId: body.quotationId,
},
});
const value = requestWork.reduce((a, c) => {
const serviceChargeStepCount = c.productService.work?.productOnWork.find(
(v) => v.productId === c.productService.productId,
)?.stepCount;
const successCount = c.stepStatus.filter(
(v) => v.workStatus === RequestWorkStatus.Completed,
).length;
const price = c.request.quotation.agentPrice ? "price" : "agentPrice";
if (serviceChargeStepCount && successCount) {
return (
a +
c.productService.product[price] -
c.productService.product.serviceCharge * successCount
);
}
return a + c.productService.product.price;
}, 0);
this.setStatus(HttpStatus.CREATED);
return record;
return await prisma.$transaction(
async (tx) => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
const last = await tx.runningNo.upsert({
where: {
key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`,
},
create: {
key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`,
value: 1,
},
update: { value: { increment: 1 } },
});
return await prisma.creditNote.create({
include: {
requestWork: {
include: {
request: true,
},
},
quotation: true,
},
data: {
reason: body.reason,
detail: body.detail,
paybackType: body.paybackType,
paybackBank: body.paybackBank,
paybackAccount: body.paybackAccount,
paybackAccountName: body.paybackAccountName,
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
value,
requestWork: {
connect: body.requestWorkId.map((v) => ({
id: v,
})),
},
quotationId: body.quotationId,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
}
@Put("{creditNoteId}")
@ -284,6 +398,7 @@ export class CreditNoteController extends Controller {
const requestWork = await prisma.requestWork.findMany({
where: {
OR: [{ creditNote: null }, { creditNoteId }],
request: {
quotation: {
id: body.quotationId,
@ -296,16 +411,55 @@ export class CreditNoteController extends Controller {
},
id: { in: body.requestWorkId },
},
include: {
stepStatus: true,
productService: {
include: {
product: true,
work: {
include: { productOnWork: true },
},
},
},
request: {
include: {
quotation: {
include: {
registeredBranch: { include: branchRelationPermInclude(req.user) },
},
},
},
},
},
});
if (requestWork.length !== body.requestWorkId.length) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Not Match", "reqNotMet");
}
const value = requestWork.reduce((a, c) => {
const serviceChargeStepCount = c.productService.work?.productOnWork.find(
(v) => v.productId === c.productService.productId,
)?.stepCount;
const successCount = c.stepStatus.filter(
(v) => v.workStatus === RequestWorkStatus.Completed,
).length;
const price = c.request.quotation.agentPrice ? "price" : "agentPrice";
if (serviceChargeStepCount && successCount) {
return (
a +
c.productService.product[price] -
c.productService.product.serviceCharge * successCount
);
}
return a + c.productService.product.price;
}, 0);
const record = await prisma.creditNote.update({
where: {
id: creditNoteId,
},
where: { id: creditNoteId },
include: {
requestWork: {
include: {
@ -315,6 +469,13 @@ export class CreditNoteController extends Controller {
quotation: true,
},
data: {
reason: body.reason,
detail: body.detail,
paybackType: body.paybackType,
paybackBank: body.paybackBank,
paybackAccount: body.paybackAccount,
paybackAccountName: body.paybackAccountName,
value,
requestWork: {
disconnect: creditNoteData.requestWork
.map((item) => ({
@ -353,3 +514,110 @@ export class CreditNoteController extends Controller {
return await prisma.creditNote.delete({ where: { id: creditNoteId } });
}
}
@Route("api/v1/credit-note/{creditNoteId}")
@Tags("Credit Note")
export class CreditNoteActionController extends Controller {
async #checkPermission(user: RequestWithUser["user"], id: string) {
const creditNoteData = await prisma.creditNote.findFirst({
where: { id },
include: {
requestWork: true,
quotation: {
include: {
registeredBranch: { include: branchRelationPermInclude(user) },
},
},
},
});
if (!creditNoteData) throw notFoundError("Credit Note");
await permissionCheck(user, creditNoteData.quotation.registeredBranch);
return creditNoteData;
}
@Post("payback-status")
@Security("keycloak", MANAGE_ROLES)
async updateStatus(
@Request() req: RequestWithUser,
@Path() creditNoteId: string,
@Body() body: PaybackStatus,
) {
await this.#checkPermission(req.user, creditNoteId);
return await prisma.creditNote.update({
where: { id: creditNoteId },
include: {
requestWork: {
include: {
request: true,
},
},
quotation: true,
},
data: {
creditNoteStatus: body === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
paybackStatus: body,
},
});
}
}
@Route("api/v1/credit-note/{creditNoteId}")
@Tags("Credit Note")
export class CreditNoteAttachmentController extends Controller {
async #checkPermission(user: RequestWithUser["user"], id: string) {
const creditNoteData = await prisma.creditNote.findFirst({
where: { id },
include: {
requestWork: true,
quotation: {
include: {
registeredBranch: { include: branchRelationPermInclude(user) },
},
},
},
});
if (!creditNoteData) throw notFoundError("Credit Note");
await permissionCheck(user, creditNoteData.quotation.registeredBranch);
return creditNoteData;
}
@Get("attachment")
@Security("keycloak")
async listAttachment(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
await this.#checkPermission(req.user, creditNoteId);
return await listFile(fileLocation.creditNote.attachment(creditNoteId));
}
@Get("attachment/{name}")
@Security("keycloak")
async getAttachment(@Path() creditNoteId: string, @Path() name: string) {
return await getFile(fileLocation.creditNote.attachment(creditNoteId, name));
}
@Head("attachment/{name}")
async headAttachment(@Path() creditNoteId: string, @Path() name: string) {
return await getPresigned("head", fileLocation.creditNote.attachment(creditNoteId, name));
}
@Put("attachment/{name}")
@Security("keycloak")
async putAttachment(
@Request() req: RequestWithUser,
@Path() creditNoteId: string,
@Path() name: string,
) {
await this.#checkPermission(req.user, creditNoteId);
return await setFile(fileLocation.creditNote.attachment(creditNoteId, name));
}
@Delete("attachment/{name}")
@Security("keycloak")
async delAttachment(
@Request() req: RequestWithUser,
@Path() creditNoteId: string,
@Path() name: string,
) {
await this.#checkPermission(req.user, creditNoteId);
return await deleteFile(fileLocation.creditNote.attachment(creditNoteId, name));
}
}

View file

@ -112,4 +112,7 @@ export const fileLocation = {
task: {
attachment: (taskId: string, name?: string) => `task/attachment-${taskId}/${name || ""}`,
},
creditNote: {
attachment: (taskId: string, name?: string) => `credit-note/attachment-${taskId}/${name || ""}`,
},
};