Compare commits

..

No commits in common. "develop" and "version-0.11.13" have entirely different histories.

54 changed files with 769 additions and 4275 deletions

View file

@ -1,22 +1,33 @@
FROM node:20-slim
FROM node:23-slim AS base
RUN apt-get update -y \
&& apt-get install -y openssl \
&& npm install -g pnpm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma prisma-kysely
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm prisma generate
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm prisma generate
RUN pnpm run build
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
FROM base AS prod
ENTRYPOINT ["/entrypoint.sh"]
ENV NODE_ENV="production"
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
COPY --from=base /app/static /app/static
RUN chmod u+x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

View file

@ -7,7 +7,6 @@
"start": "node ./dist/app.js",
"dev": "nodemon",
"check": "tsc --noEmit",
"test": "vitest",
"format": "prettier --write .",
"debug": "nodemon",
"build": "tsoa spec-and-routes && tsc",
@ -28,26 +27,22 @@
"@types/multer": "^1.4.12",
"@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17",
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
"prisma": "6.16.2",
"prisma": "^6.3.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
"vitest": "^3.1.4"
"typescript": "^5.7.2"
},
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
"@prisma/client": "6.16.2",
"@prisma/client": "^6.3.0",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4",
"canvas": "^3.1.0",
"barcode": "^0.1.0",
"cors": "^2.8.5",
"cron": "^3.3.1",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
"dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0",
@ -55,15 +50,12 @@
"exceljs": "^4.4.0",
"express": "^4.21.2",
"fast-jwt": "^5.0.5",
"html-to-text": "^9.0.5",
"jsbarcode": "^3.11.6",
"json-2-csv": "^5.5.8",
"kysely": "^0.27.5",
"minio": "^8.0.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
"pnpm": "^10.18.3",
"prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6",
"thai-baht-text": "^2.0.5",

1532
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the column `importNationality` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "importNationality";
-- CreateTable
CREATE TABLE "UserImportNationality" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "UserImportNationality_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "QuotationWorker" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -1,5 +0,0 @@
-- DropForeignKey
ALTER TABLE "UserImportNationality" DROP CONSTRAINT "UserImportNationality_userId_fkey";
-- AddForeignKey
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Quotation" ADD COLUMN "sellerId" TEXT;
-- AddForeignKey
ALTER TABLE "Quotation" ADD CONSTRAINT "Quotation_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,36 +0,0 @@
/*
Warnings:
- You are about to drop the column `businessType` on the `CustomerBranch` table. All the data in the column will be lost.
- You are about to drop the column `customerName` on the `CustomerBranch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "CustomerBranch" DROP COLUMN "businessType",
DROP COLUMN "customerName",
ADD COLUMN "businessTypeId" TEXT;
-- AlterTable
ALTER TABLE "EmployeeVisa" ADD COLUMN "reportDate" DATE;
-- CreateTable
CREATE TABLE "BusinessType" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdByUserId" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedByUserId" TEXT,
CONSTRAINT "BusinessType_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_businessTypeId_fkey" FOREIGN KEY ("businessTypeId") REFERENCES "BusinessType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BusinessType" ADD CONSTRAINT "BusinessType_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "addressForeign" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "districtText" TEXT,
ADD COLUMN "provinceText" TEXT,
ADD COLUMN "subDistrictText" TEXT,
ADD COLUMN "zipCodeText" TEXT;

View file

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "districtTextEN" TEXT,
ADD COLUMN "provinceTextEN" TEXT,
ADD COLUMN "subDistrictTextEN" TEXT;

View file

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "RequestWorkStepStatus" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "account" TEXT,
ADD COLUMN "channel" TEXT,
ADD COLUMN "reference" TEXT;

View file

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "public"."Product" ADD COLUMN "flowAccountProductIdAgentPrice" TEXT,
ADD COLUMN "flowAccountProductIdSellPrice" TEXT;

View file

@ -366,14 +366,6 @@ enum UserType {
AGENCY
}
model UserImportNationality {
id String @id @default(cuid())
name String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
}
model User {
id String @id @default(cuid())
@ -398,24 +390,14 @@ model User {
street String?
streetEN String?
addressForeign Boolean @default(false)
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
provinceText String?
provinceTextEN String?
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String?
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
districtId String?
districtText String?
districtTextEN String?
district District? @relation(fields: [districtId], references: [id], onDelete: SetNull)
districtId String?
subDistrictText String?
subDistrictTextEN String?
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
subDistrictId String?
zipCodeText String?
subDistrict SubDistrict? @relation(fields: [subDistrictId], references: [id], onDelete: SetNull)
subDistrictId String?
email String
telephoneNo String
@ -442,7 +424,7 @@ model User {
licenseExpireDate DateTime? @db.Date
sourceNationality String?
importNationality UserImportNationality[]
importNationality String?
trainingPlace String?
responsibleArea UserResponsibleArea[]
@ -511,8 +493,6 @@ model User {
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
institutionCreated Institution[] @relation("InstitutionCreatedByUser")
institutionUpdated Institution[] @relation("InstitutionUpdatedByUser")
businessTypeCreated BusinessType[] @relation("BusinessTypeCreatedByUser")
businessTypeUpdated BusinessType[] @relation("BusinessTypeUpdatedByUser")
requestWorkStepStatus RequestWorkStepStatus[]
userTask UserTask[]
@ -523,7 +503,6 @@ model User {
contactName String?
contactTel String?
quotation Quotation[]
}
model UserResponsibleArea {
@ -562,9 +541,10 @@ model Customer {
}
model CustomerBranch {
id String @id @default(cuid())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
id String @id @default(cuid())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
customerName String?
code String
codeCustomer String
@ -626,8 +606,7 @@ model CustomerBranch {
agentUser User? @relation(fields: [agentUserId], references: [id], onDelete: SetNull)
// NOTE: Business
businessTypeId String?
businessType BusinessType? @relation(fields: [businessTypeId], references: [id], onDelete: SetNull)
businessType String
jobPosition String
jobDescription String
payDate String
@ -786,21 +765,6 @@ model CustomerBranchVatRegis {
customerBranch CustomerBranch @relation(fields: [customerBranchId], references: [id], onDelete: Cascade)
}
model BusinessType {
id String @id @default(cuid())
name String
nameEN String
createdAt DateTime @default(now())
createdBy User? @relation(name: "BusinessTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @updatedAt
updatedBy User? @relation(name: "BusinessTypeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
customerBranch CustomerBranch[]
}
model Employee {
id String @id @default(cuid())
@ -815,10 +779,9 @@ model Employee {
lastName String?
lastNameEN String?
dateOfBirth DateTime? @db.Date
gender String
nationality String
otherNationality String?
dateOfBirth DateTime? @db.Date
gender String
nationality String
address String?
addressEN String?
@ -893,19 +856,18 @@ model EmployeePassport {
issuePlace String
previousPassportRef String?
workerStatus String?
nationality String?
otherNationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
workerStatus String?
nationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
@ -922,9 +884,8 @@ model EmployeeVisa {
entryCount Int
issueCountry String
issuePlace String
issueDate DateTime @db.Date
expireDate DateTime @db.Date
reportDate DateTime? @db.Date
issueDate DateTime @db.Date
expireDate DateTime @db.Date
mrz String?
remark String?
@ -1243,9 +1204,6 @@ model Product {
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String
flowAccountProductIdSellPrice String?
flowAccountProductIdAgentPrice String?
workProduct WorkProduct[]
quotationProductServiceList QuotationProductServiceList[]
taskProduct TaskProduct[]
@ -1418,9 +1376,6 @@ model Quotation {
invoice Invoice[]
creditNote CreditNote[]
seller User? @relation(fields: [sellerId], references: [id], onDelete: Cascade)
sellerId String?
}
model QuotationPaySplit {
@ -1445,9 +1400,6 @@ model QuotationWorker {
employeeId String
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
quotationId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model QuotationProductServiceList {
@ -1527,11 +1479,8 @@ model Payment {
paymentStatus PaymentStatus
amount Float
date DateTime?
channel String?
account String?
reference String?
amount Float
date DateTime?
createdAt DateTime @default(now())
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
@ -1618,7 +1567,6 @@ model RequestWork {
model RequestWorkStepStatus {
step Int
workStatus RequestWorkStatus @default(Pending)
updatedAt DateTime @default(now()) @updatedAt
requestWork RequestWork @relation(fields: [requestWorkId], references: [id], onDelete: Cascade)
requestWorkId String

View file

@ -1,5 +1,4 @@
import { createCanvas } from "canvas";
import JsBarcode from "jsbarcode";
import barcode from "barcode";
import createReport from "docx-templates";
import ThaiBahtText from "thai-baht-text";
import { District, Province, SubDistrict } from "@prisma/client";
@ -34,14 +33,8 @@ const quotationData = (id: string) =>
},
},
customerBranch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: true,
businessType: true,
province: true,
district: true,
subDistrict: true,
@ -118,12 +111,12 @@ export class DocTemplateController extends Controller {
) {
const ret = await edmList(
"file",
templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH,
templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH,
);
if (ret) return ret.map((v) => v.fileName);
}
return await listFile(
(templateGroup ? [...DOCUMENT_PATH, templateGroup] : DOCUMENT_PATH).join("/") + "/",
(templateGroup ? [templateGroup, ...DOCUMENT_PATH] : DOCUMENT_PATH).join("/") + "/",
);
}
@ -260,23 +253,13 @@ export class DocTemplateController extends Controller {
thaiBahtText: (input: string | number) => {
ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input);
},
barcode: async (data: string, width?: number, height?: number) =>
new Promise<{
width: number;
height: number;
data: string;
extension: string;
}>((resolve) => {
const canvas = createCanvas(400, 100);
JsBarcode(canvas, data);
resolve({
width: width ?? 8,
height: height ?? 3,
data: canvas.toDataURL("image/jpeg").slice("data:image/jpeg;base64".length),
extension: ".jpeg",
});
}),
barcode: async (data: string) =>
new Promise<string>((resolve, reject) =>
barcode("code39", { data, width: 400, height: 100 }).getBase64((err, data) => {
if (!err) return resolve(data);
return reject(err);
}),
),
},
}).then(Buffer.from);
@ -293,7 +276,6 @@ function replaceEmptyField<T>(data: T): T {
}
type FullAddress = {
addressForeign?: boolean;
address: string;
addressEN: string;
moo?: string;
@ -302,14 +284,8 @@ type FullAddress = {
soiEN?: string;
street?: string;
streetEN?: string;
provinceText?: string | null;
provinceTextEN?: string | null;
province?: Province | null;
districtText?: string | null;
districtTextEN?: string | null;
district?: District | null;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrict?: SubDistrict | null;
en?: boolean;
};
@ -343,22 +319,13 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soi) fragments.push(`ซอย ${addr.soi},`);
if (addr.street) fragments.push(`ถนน${addr.street},`);
if (!addr.addressForeign && addr.subDistrict) {
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name}`);
if (addr.subDistrict) {
fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name},`);
}
if (addr.addressForeign && addr.subDistrictText) {
fragments.push(`ตำบล${addr.subDistrictText}`);
if (addr.district) {
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name},`);
}
if (!addr.addressForeign && addr.district) {
fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name}`);
}
if (addr.addressForeign && addr.districtText) {
fragments.push(`อำเภอ${addr.districtText}`);
}
if (!addr.addressForeign && addr.province) fragments.push(`จังหวัด${addr.province.name}`);
if (addr.addressForeign && addr.provinceText) fragments.push(`จังหวัด${addr.provinceText}`);
if (addr.province) fragments.push(`จังหวัด${addr.province.name},`);
break;
default:
@ -367,31 +334,14 @@ function addressFull(addr: FullAddress, lang: "th" | "en" = "en") {
if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`);
if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`);
if (!addr.addressForeign && addr.subDistrict) {
if (addr.subDistrict) {
fragments.push(`${addr.subDistrict.nameEN} sub-district,`);
}
if (addr.addressForeign && addr.subDistrictTextEN) {
fragments.push(`${addr.subDistrictTextEN} sub-district,`);
}
if (!addr.addressForeign && addr.district) {
fragments.push(`${addr.district.nameEN} district,`);
}
if (addr.addressForeign && addr.districtTextEN) {
fragments.push(`${addr.districtTextEN} district,`);
}
if (!addr.addressForeign && addr.province) {
fragments.push(`${addr.province.nameEN},`);
}
if (addr.addressForeign && addr.provinceTextEN) {
fragments.push(`${addr.provinceTextEN} district,`);
}
if (addr.district) fragments.push(`${addr.district.nameEN} district,`);
if (addr.province) fragments.push(`${addr.province.nameEN},`);
break;
}
if (addr.subDistrict) fragments.push(addr.subDistrict.zipCode);
return fragments.join(" ");
}
@ -404,9 +354,6 @@ function gender(text: string, lang: "th" | "en" = "en") {
}
}
/**
* @deprecated
*/
function businessType(text: string, lang: "th" | "en" = "en") {
switch (lang) {
case "th":

View file

@ -618,22 +618,9 @@ export class StatsController extends Controller {
startDate = dayjs(startDate).startOf("month").add(1, "month").toDate();
}
const invoices = await tx.invoice.findMany({
select: { id: true },
where: {
quotation: {
quotationStatus: { notIn: [QuotationStatus.Canceled] },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
});
if (invoices.length === 0) return [];
return await Promise.all(
months.map(async (v) => {
const date = dayjs(v);
return {
month: date.format("MM"),
year: date.format("YYYY"),
@ -642,7 +629,11 @@ export class StatsController extends Controller {
_sum: { amount: true },
where: {
createdAt: { gte: v, lte: date.endOf("month").toDate() },
invoiceId: { in: invoices.map((v) => v.id) },
invoice: {
quotation: {
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
by: "paymentStatus",
})

View file

@ -47,20 +47,16 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
return MANAGE_ROLES.some((v) => user.roles?.includes(v));
}
function globalAllowView(user: RequestWithUser["user"]) {
return MANAGE_ROLES.concat("head_of_accountant", "head_of_sale").some((v) =>
user.roles?.includes(v),
);
}
type BranchCreate = {
@ -151,7 +147,7 @@ type BranchUpdate = {
}[];
};
const permissionCond = createPermCondition(globalAllow);
const permissionCond = createPermCondition(globalAllowView);
const permissionCheck = createPermCheck(globalAllow);
@Route("api/v1/branch")
@ -330,7 +326,7 @@ export class BranchController extends Controller {
district: true,
subDistrict: true,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
orderBy: { code: "asc" },
}
: false,
bank: true,
@ -374,7 +370,7 @@ export class BranchController extends Controller {
bank: true,
contact: includeContact,
},
orderBy: [{ statusOrder: "asc" }, { code: "asc" }],
orderBy: { code: "asc" },
},
bank: true,
contact: includeContact,
@ -387,14 +383,6 @@ export class BranchController extends Controller {
return record;
}
@Get("{branchId}/bank")
@Security("keycloak")
async getBranchBankById(@Path() branchId: string) {
return await prisma.branchBank.findMany({
where: { branchId },
});
}
@Post()
@Security("keycloak", MANAGE_ROLES)
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {

View file

@ -20,19 +20,10 @@ import { RequestWithUser } from "../interfaces/user";
import { branchRelationPermInclude, createPermCheck } from "../services/permission";
import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
const listAllowed = ["system", "head_of_admin", "admin"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
}

View file

@ -61,17 +61,10 @@ if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"branch_admin",
"branch_manager",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive"];
const listAllowed = ["system", "head_of_admin"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
}
@ -106,12 +99,11 @@ type UserCreate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string[] | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
addressForeign?: boolean;
address: string;
addressEN: string;
soi?: string | null;
@ -123,16 +115,9 @@ type UserCreate = {
email: string;
telephoneNo: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null;
zipCodeText?: string | null;
selectedImage?: string;
@ -141,8 +126,8 @@ type UserCreate = {
remark?: string;
agencyStatus?: string;
contactName?: string | null;
contactTel?: string | null;
contactName?: string;
contactTel?: string;
};
type UserUpdate = {
@ -159,9 +144,9 @@ type UserUpdate = {
namePrefix?: string | null;
firstName?: string;
firstNameEN?: string;
firstNameEN: string;
middleName?: string | null;
middleNameEN?: string | null;
middleNameEN: string | null;
lastName?: string;
lastNameEN?: string;
gender?: string;
@ -176,12 +161,11 @@ type UserUpdate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string[] | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
addressForeign?: boolean;
address?: string;
addressEN?: string;
soi?: string | null;
@ -195,24 +179,17 @@ type UserUpdate = {
selectedImage?: string;
subDistrictText?: string | null;
subDistrictTextEN?: string | null;
subDistrictId?: string | null;
districtText?: string | null;
districtTextEN?: string | null;
districtId?: string | null;
provinceText?: string | null;
provinceTextEN?: string | null;
provinceId?: string | null;
zipCodeText?: string | null;
branchId?: string | string[];
remark?: string;
agencyStatus?: string;
contactName?: string | null;
contactTel?: string | null;
contactName?: string;
contactTel?: string;
};
const permissionCondCompany = createPermCondition((_) => true);
@ -406,7 +383,6 @@ export class UserController extends Controller {
prisma.user.findMany({
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
importNationality: true,
responsibleArea: true,
province: true,
district: true,
@ -425,7 +401,6 @@ export class UserController extends Controller {
return {
result: result.map((v) => ({
...v,
importNationality: v.importNationality.map((v) => v.name),
responsibleArea: v.responsibleArea.map((v) => v.area),
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
})),
@ -440,7 +415,6 @@ export class UserController extends Controller {
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -452,11 +426,7 @@ export class UserController extends Controller {
if (!record) throw notFoundError("User");
const { importNationality, ...rest } = record;
return Object.assign(rest, {
importNationality: importNationality.map((v) => v.name),
});
return record;
}
@Post()
@ -558,9 +528,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
username,
userRole: role.name,
@ -716,7 +683,6 @@ export class UserController extends Controller {
const record = await prisma.user.update({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -731,10 +697,6 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
deleteMany: {},
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
userRole,
province: connectOrDisconnect(provinceId),

View file

@ -23,16 +23,15 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type CustomerBranchCitizenPayload = {

View file

@ -47,18 +47,15 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -87,6 +84,7 @@ export type CustomerBranchCreate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -110,7 +108,7 @@ export type CustomerBranchCreate = {
contactName: string;
agentUserId?: string;
businessTypeId?: string;
businessType: string;
jobPosition: string;
jobDescription: string;
payDate: string;
@ -144,6 +142,7 @@ export type CustomerBranchUpdate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -167,7 +166,7 @@ export type CustomerBranchUpdate = {
contactName?: string;
agentUserId?: string;
businessTypeId?: string;
businessType?: string;
jobPosition?: string;
jobDescription?: string;
payDate?: string;
@ -202,6 +201,7 @@ export class CustomerBranchController extends Controller {
) {
const where = {
OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
@ -238,11 +238,6 @@ export class CustomerBranchController extends Controller {
const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({
orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }],
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: includeCustomer,
province: true,
@ -251,7 +246,6 @@ export class CustomerBranchController extends Controller {
createdBy: true,
updatedBy: true,
_count: true,
businessType: true,
},
where,
take: pageSize,
@ -267,11 +261,6 @@ export class CustomerBranchController extends Controller {
@Security("keycloak")
async getById(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
include: {
customer: true,
province: true,
@ -279,7 +268,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
where: { id: branchId },
});
@ -362,11 +350,6 @@ export class CustomerBranchController extends Controller {
include: branchRelationPermInclude(req.user),
},
branch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1,
orderBy: { createdAt: "asc" },
},
@ -395,15 +378,7 @@ export class CustomerBranchController extends Controller {
(v) => (v.headOffice || v).code,
);
const {
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
const record = await prisma.$transaction(
async (tx) => {
@ -446,7 +421,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
data: {
...rest,
@ -458,7 +432,6 @@ export class CustomerBranchController extends Controller {
province: connectOrNot(provinceId),
district: connectOrNot(districtId),
subDistrict: connectOrNot(subDistrictId),
businessType: connectOrNot(businessTypeId),
createdBy: { connect: { id: req.user.sub } },
updatedBy: { connect: { id: req.user.sub } },
},
@ -489,7 +462,6 @@ export class CustomerBranchController extends Controller {
},
},
},
businessType: true,
},
});
@ -534,15 +506,7 @@ export class CustomerBranchController extends Controller {
await permissionCheck(req.user, customer.registeredBranch);
}
const {
provinceId,
districtId,
subDistrictId,
customerId,
agentUserId,
businessTypeId,
...rest
} = body;
const { provinceId, districtId, subDistrictId, customerId, agentUserId, ...rest } = body;
return await prisma.customerBranch.update({
where: { id: branchId },
@ -552,7 +516,6 @@ export class CustomerBranchController extends Controller {
subDistrict: true,
createdBy: true,
updatedBy: true,
businessType: true,
},
data: {
...rest,
@ -562,7 +525,6 @@ export class CustomerBranchController extends Controller {
province: connectOrDisconnect(provinceId),
district: connectOrDisconnect(districtId),
subDistrict: connectOrDisconnect(subDistrictId),
businessType: connectOrNot(businessTypeId),
updatedBy: { connect: { id: req.user.sub } },
},
});
@ -581,7 +543,6 @@ export class CustomerBranchController extends Controller {
},
},
},
businessType: true,
},
});
@ -634,11 +595,10 @@ export class CustomerBranchFileController extends Controller {
},
},
},
businessType: true,
},
});
if (!data) throw notFoundError("Customer Branch");
await permissionCheckCompany(user, data.customer.registeredBranch);
await permissionCheck(user, data.customer.registeredBranch);
}
@Get("attachment")

View file

@ -37,24 +37,20 @@ import {
} from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation";
import { json2csv } from "json-2-csv";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -86,6 +82,7 @@ export type CustomerCreate = {
authorizedCapital?: string;
authorizedName?: string;
authorizedNameEN?: string;
customerName?: string;
telephoneNo: string;
@ -109,7 +106,7 @@ export type CustomerCreate = {
contactName: string;
agentUserId?: string;
businessTypeId?: string | null;
businessType: string;
jobPosition: string;
jobDescription: string;
payDate: string;
@ -170,14 +167,11 @@ export class CustomerController extends Controller {
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Query() businessTypeId?: string,
@Query() provinceId?: string,
@Query() districtId?: string,
@Query() subDistrictId?: string,
) {
const where = {
OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [
{ branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { customerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstName: { contains: query, mode: "insensitive" } } } },
@ -196,35 +190,6 @@ export class CustomerController extends Controller {
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
},
},
branch: {
some: {
AND: [
businessTypeId
? {
OR: [{ businessType: { id: businessTypeId } }],
}
: {},
provinceId
? {
OR: [{ province: { id: provinceId } }],
}
: {},
districtId
? {
OR: [{ district: { id: districtId } }],
}
: {},
subDistrictId
? {
OR: [{ subDistrict: { id: subDistrictId } }],
}
: {},
],
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerWhereInput;
@ -235,16 +200,10 @@ export class CustomerController extends Controller {
branch: includeBranch
? {
include: {
businessType: true,
province: true,
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
}
: {
@ -253,17 +212,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
take: 1,
orderBy: { createdAt: "asc" },
},
createdBy: true,
updatedBy: true,
// businessType:true
},
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
where,
@ -288,11 +241,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
orderBy: { createdAt: "asc" },
},
createdBy: true,
@ -364,11 +312,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
createdBy: true,
updatedBy: true,
@ -380,8 +323,6 @@ export class CustomerController extends Controller {
...v,
code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + i}`.padStart(2, "0")}`,
codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""),
businessType: connectOrNot(v.businessTypeId),
businessTypeId: undefined,
agentUser: connectOrNot(v.agentUserId),
agentUserId: undefined,
province: connectOrNot(v.provinceId),
@ -468,11 +409,6 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
createdBy: true,
updatedBy: true,
@ -511,13 +447,7 @@ export class CustomerController extends Controller {
await deleteFolder(`customer/${customerId}`);
const data = await tx.customer.delete({
include: {
branch: {
omit: {
otpCode: true,
otpExpires: true,
userId: true,
},
},
branch: true,
registeredBranch: {
include: {
headOffice: true,
@ -612,52 +542,3 @@ export class CustomerImageController extends Controller {
await deleteFile(fileLocation.customer.img(customerId, name));
}
}
@Route("api/v1/customer-export")
@Tags("Customer")
export class CustomerExportController extends CustomerController {
@Get()
@Security("keycloak")
async exportCustomer(
@Request() req: RequestWithUser,
@Query() customerType?: CustomerType,
@Query() query: string = "",
@Query() status?: Status,
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() includeBranch: boolean = false,
@Query() company: boolean = false,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Query() businessTypeId?: string,
@Query() provinceId?: string,
@Query() districtId?: string,
@Query() subDistrictId?: string,
) {
const ret = await this.list(
req,
customerType,
query,
status,
page,
pageSize,
includeBranch,
company,
activeBranchOnly,
startDate,
endDate,
businessTypeId,
provinceId,
districtId,
subDistrictId,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(
ret.result.map((v) => Object.assign(v, { branch: v.branch.at(0) ?? null })),
{ useDateIso8601Format: true, expandNestedObjects: true },
);
}
}

View file

@ -23,18 +23,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeCheckupPayload = {

View file

@ -42,7 +42,6 @@ import {
listFile,
setFile,
} from "../utils/minio";
import { json2csv } from "json-2-csv";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
@ -52,23 +51,17 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
const permissionCond = createPermCondition(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
const permissionCheck = createPermCheck(globalAllow);
type EmployeeCreate = {
@ -81,7 +74,6 @@ type EmployeeCreate = {
dateOfBirth?: Date | null;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
@ -115,10 +107,9 @@ type EmployeeUpdate = {
nrcNo?: string | null;
dateOfBirth?: Date | null;
dateOfBirth?: Date;
gender?: string;
nationality?: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName?: string;
@ -151,18 +142,9 @@ type EmployeeUpdate = {
export class EmployeeController extends Controller {
@Get("stats")
@Security("keycloak")
async getEmployeeStats(@Request() req: RequestWithUser, @Query() customerBranchId?: string) {
async getEmployeeStats(@Query() customerBranchId?: string) {
return await prisma.employee.count({
where: {
customerBranchId,
customerBranch: {
customer: isSystem(req.user)
? undefined
: {
registeredBranch: { OR: permissionCond(req.user) },
},
},
},
where: { customerBranchId },
});
}
@ -250,6 +232,7 @@ export class EmployeeController extends Controller {
endDate,
);
}
@Post("list")
@Security("keycloak")
async listByCriteria(
@ -671,7 +654,7 @@ export class EmployeeFileController extends Controller {
},
});
if (!data) throw notFoundError("Employee");
await permissionCheckCompany(user, data.customerBranch.customer.registeredBranch);
await permissionCheck(user, data.customerBranch.customer.registeredBranch);
}
@Get("image")
@ -927,55 +910,3 @@ export class EmployeeFileController extends Controller {
return await deleteFile(fileLocation.employee.inCountryNotice(employeeId, noticeId));
}
}
@Route("api/v1/employee-export")
@Tags("Employee")
export class EmployeeExportController extends EmployeeController {
@Get()
@Security("keycloak")
async exportEmployee(
@Request() req: RequestWithUser,
@Query() zipCode?: string,
@Query() gender?: string,
@Query() status?: Status,
@Query() visa?: boolean,
@Query() passport?: boolean,
@Query() customerId?: string,
@Query() customerBranchId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const ret = await this.listByCriteria(
req,
zipCode,
gender,
status,
visa,
passport,
customerId,
customerBranchId,
query,
page,
pageSize,
activeOnly,
startDate,
endDate,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(
ret.result.map((v) =>
Object.assign(v, {
employeePassport: v.employeePassport?.at(0) ?? null,
employeeVisa: v.employeeVisa?.at(0) ?? null,
}),
),
{ useDateIso8601Format: true },
);
}
}

View file

@ -23,18 +23,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeOtherInfoPayload = {

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeePassportPayload = {
@ -47,7 +43,6 @@ type EmployeePassportPayload = {
workerStatus: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string | null;
firstName: string;
firstNameEN: string;

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeVisaPayload = {
@ -44,7 +40,6 @@ type EmployeeVisaPayload = {
issuePlace: string;
issueDate: Date;
expireDate: Date;
reportDate?: Date | null;
mrz?: string | null;
remark?: string | null;

View file

@ -22,18 +22,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
type EmployeeWorkPayload = {

View file

@ -44,30 +44,14 @@ type WorkflowPayload = {
status?: Status;
};
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
}
const permissionCondCompany = createPermCondition(globalAllow);
const permissionCheckCompany = createPermCheck(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheckCompany = createPermCheck((_) => true);
@Route("api/v1/workflow-template")
@Tags("Workflow")
@Security("keycloak")
export class FlowTemplateController extends Controller {
@Get()
@Security("keycloak")
async getFlowTemplate(
@Request() req: RequestWithUser,
@Query() page: number = 1,
@ -134,7 +118,6 @@ export class FlowTemplateController extends Controller {
}
@Get("{templateId}")
@Security("keycloak")
async getFlowTemplateById(@Request() _req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findFirst({
include: {
@ -167,7 +150,6 @@ export class FlowTemplateController extends Controller {
}
@Post()
@Security("keycloak", MANAGE_ROLES)
async createFlowTemplate(@Request() req: RequestWithUser, @Body() body: WorkflowPayload) {
const where = {
OR: [
@ -248,7 +230,6 @@ export class FlowTemplateController extends Controller {
}
@Put("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async updateFlowTemplate(
@Request() req: RequestWithUser,
@Path() templateId: string,
@ -334,7 +315,6 @@ export class FlowTemplateController extends Controller {
}
@Delete("{templateId}")
@Security("keycloak", MANAGE_ROLES)
async deleteFlowTemplateById(@Request() req: RequestWithUser, @Path() templateId: string) {
const record = await prisma.workflowTemplate.findUnique({
where: { id: templateId },

View file

@ -95,17 +95,6 @@ type InstitutionUpdatePayload = {
}[];
};
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
@Route("api/v1/institution")
@Tags("Institution")
export class InstitutionController extends Controller {
@ -196,7 +185,7 @@ export class InstitutionController extends Controller {
}
@Post()
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("createInstitution")
async createInstitution(
@Body()
@ -240,7 +229,7 @@ export class InstitutionController extends Controller {
}
@Put("{institutionId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("updateInstitution")
async updateInstitution(
@Path() institutionId: string,
@ -289,7 +278,7 @@ export class InstitutionController extends Controller {
}
@Delete("{institutionId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
@OperationId("deleteInstitution")
async deleteInstitution(@Path() institutionId: string) {
return await prisma.$transaction(async (tx) => {
@ -361,7 +350,7 @@ export class InstitutionFileController extends Controller {
}
@Put("image/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async putImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -375,7 +364,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("image/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async delImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -405,7 +394,7 @@ export class InstitutionFileController extends Controller {
}
@Put("attachment/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async putAttachment(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -416,7 +405,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("attachment/{name}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async delAttachment(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -447,7 +436,7 @@ export class InstitutionFileController extends Controller {
}
@Put("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async putBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,
@ -461,7 +450,7 @@ export class InstitutionFileController extends Controller {
}
@Delete("bank-qr/{bankId}")
@Security("keycloak", MANAGE_ROLES)
@Security("keycloak")
async delBankImage(
@Request() req: RequestWithUser,
@Path() institutionId: string,

View file

@ -29,23 +29,14 @@ type InvoicePayload = {
installmentNo: number[];
};
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
@Route("/api/v1/invoice")
@ -117,6 +108,7 @@ export class InvoiceController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
@ -192,7 +184,7 @@ export class InvoiceController extends Controller {
@Post()
@OperationId("createInvoice")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) {
const [quotation] = await prisma.$transaction([
prisma.quotation.findUnique({
@ -237,7 +229,7 @@ export class InvoiceController extends Controller {
title: "ใบแจ้งหนี้ใหม่ / New Invoice",
detail: "รหัส / code : " + record.code,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "branch_accountant" } },
groupReceiver: { create: { name: "accountant" } },
},
});

View file

@ -30,23 +30,19 @@ import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } fr
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import spreadsheet from "../utils/spreadsheet";
import flowAccount from "../services/flowaccount";
import { json2csv } from "json-2-csv";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -78,7 +74,6 @@ type ProductCreate = {
type ProductUpdate = {
status?: "ACTIVE" | "INACTIVE";
code?: string;
name?: string;
detail?: string;
process?: number;
@ -302,21 +297,13 @@ export class ProductController extends Controller {
},
update: { value: { increment: 1 } },
});
const listId = await flowAccount.createProducts(
`${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
body,
);
return await tx.product.create({
return await prisma.product.create({
include: {
createdBy: true,
updatedBy: true,
},
data: {
...body,
flowAccountProductIdAgentPrice: `${listId.data.productIdAgentPrice}`,
flowAccountProductIdSellPrice: `${listId.data.productIdSellPrice}`,
document: body.document
? {
createMany: { data: body.document.map((v) => ({ name: v })) },
@ -390,30 +377,6 @@ export class ProductController extends Controller {
await permissionCheck(req.user, productGroup.registeredBranch);
}
if (
product.flowAccountProductIdSellPrice !== null &&
product.flowAccountProductIdAgentPrice !== null
) {
const mergedBody = {
...body,
code: body.code ?? product.code,
price: body.price ?? product.price,
agentPrice: body.agentPrice ?? product.agentPrice,
serviceCharge: body.serviceCharge ?? product.serviceCharge,
vatIncluded: body.vatIncluded ?? product.vatIncluded,
agentPriceVatIncluded: body.agentPriceVatIncluded ?? product.agentPriceVatIncluded,
serviceChargeVatIncluded: body.serviceChargeVatIncluded ?? product.serviceChargeVatIncluded,
};
await flowAccount.editProducts(
product.flowAccountProductIdSellPrice,
product.flowAccountProductIdAgentPrice,
mergedBody,
);
} else {
throw notFoundError("FlowAccountProductId");
}
const record = await prisma.product.update({
include: {
productGroup: true,
@ -476,18 +439,6 @@ export class ProductController extends Controller {
if (record.status !== Status.CREATED) throw isUsedError("Product");
if (
record.flowAccountProductIdSellPrice !== null &&
record.flowAccountProductIdAgentPrice !== null
) {
await Promise.all([
flowAccount.deleteProduct(record.flowAccountProductIdSellPrice),
flowAccount.deleteProduct(record.flowAccountProductIdAgentPrice),
]);
} else {
throw notFoundError("FlowAccountProductId");
}
await deleteFolder(fileLocation.product.img(productId));
return await prisma.product.delete({
@ -689,43 +640,3 @@ export class ProductFileController extends Controller {
return await deleteFile(fileLocation.product.img(productId, name));
}
}
@Route("api/v1/product-export")
@Tags("Product")
export class ProductExportController extends ProductController {
@Get()
@Security("keycloak")
async exportCustomer(
@Request() req: RequestWithUser,
@Query() status?: Status,
@Query() shared?: boolean,
@Query() productGroupId?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() orderField?: keyof Product,
@Query() orderBy?: "asc" | "desc",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const ret = await this.getProduct(
req,
status,
shared,
productGroupId,
query,
page,
pageSize,
orderField,
orderBy,
activeOnly,
startDate,
endDate,
);
this.setHeader("Content-Type", "text/csv");
return json2csv(ret.result, { useDateIso8601Format: true, expandNestedObjects: true });
}
}

View file

@ -35,7 +35,7 @@ type ProductGroupCreate = {
remark: string;
status?: Status;
shared?: boolean;
registeredBranchId?: string;
registeredBranchId: string;
};
type ProductGroupUpdate = {
@ -51,16 +51,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCond = createPermCondition((_) => true);
@ -159,23 +157,7 @@ export class ProductGroup extends Controller {
@Post()
@Security("keycloak", MANAGE_ROLES)
async createProductGroup(@Request() req: RequestWithUser, @Body() body: ProductGroupCreate) {
const userAffiliatedBranch = await prisma.branch.findFirst({
include: branchRelationPermInclude(req.user),
where: body.registeredBranchId
? { id: body.registeredBranchId }
: {
user: { some: { userId: req.user.sub } },
},
});
if (!userAffiliatedBranch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
"reqMinAffilatedBranch",
);
}
let company = await permissionCheck(req.user, userAffiliatedBranch).then(
let company = await permissionCheck(req.user, body.registeredBranchId).then(
(v) => (v.headOffice || v).code,
);
@ -199,7 +181,6 @@ export class ProductGroup extends Controller {
},
data: {
...body,
registeredBranchId: userAffiliatedBranch.id,
statusOrder: +(body.status === "INACTIVE"),
code: `G${last.value.toString().padStart(2, "0")}`,
createdByUserId: req.user.sub,

View file

@ -42,16 +42,14 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = MANAGE_ROLES;
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);

View file

@ -26,20 +26,11 @@ import flowAccount from "../services/flowaccount";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -110,18 +101,10 @@ export class QuotationPayment extends Controller {
}
@Put("{paymentId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async updatePayment(
@Path() paymentId: string,
@Body()
body: {
amount?: number;
date?: Date;
paymentStatus?: PaymentStatus;
channel?: string | null;
account?: string | null;
reference?: string | null;
},
@Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
@Request() req: RequestWithUser,
) {
const record = await prisma.payment.findUnique({
@ -152,18 +135,7 @@ export class QuotationPayment extends Controller {
if (!record) throw notFoundError("Payment");
if (record.paymentStatus === "PaymentSuccess") {
const { channel, account, reference } = body;
return await prisma.payment.update({
where: { id: paymentId, invoice: { quotationId: record.invoice.quotationId } },
data: {
channel,
account,
reference,
updatedByUserId: req.user.sub,
},
});
}
if (record.paymentStatus === "PaymentSuccess") return record;
return await prisma.$transaction(async (tx) => {
const current = new Date();
@ -209,7 +181,6 @@ export class QuotationPayment extends Controller {
await tx.quotation
.update({
include: { requestData: true },
where: { id: quotation.id },
data: {
quotationStatus:
@ -267,17 +238,6 @@ export class QuotationPayment extends Controller {
receiverId: res.createdByUserId,
},
});
if (quotation.quotationStatus === "PaymentInProcess") {
await prisma.notification.create({
data: {
title: "รายการคำขอใหม่ / New Request",
detail: "รหัส / code : " + res.requestData.map((v) => v.code).join(", "),
registeredBranchId: res.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}
});
return payment;

View file

@ -55,7 +55,6 @@ type QuotationCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -84,8 +83,6 @@ type QuotationCreate = {
installmentNo?: number;
workerIndex?: number[];
}[];
sellerId?: string;
};
type QuotationUpdate = {
@ -115,7 +112,6 @@ type QuotationUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName?: string;
@ -144,8 +140,6 @@ type QuotationUpdate = {
installmentNo?: number;
workerIndex?: number[];
}[];
sellerId?: string;
};
const VAT_DEFAULT = config.vat;
@ -154,16 +148,15 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCheckCompany = createPermCheck((_) => true);
@ -215,7 +208,6 @@ export class QuotationController extends Controller {
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
@Query() sellerId?: string,
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
@ -225,6 +217,7 @@ export class QuotationController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
@ -263,7 +256,6 @@ export class QuotationController extends Controller {
}
: undefined,
...whereDateQuery(startDate, endDate),
sellerId: sellerId,
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -421,7 +413,7 @@ export class QuotationController extends Controller {
}
@Post()
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
const ids = {
employee: body.worker.filter((v) => typeof v === "string"),
@ -527,15 +519,16 @@ export class QuotationController extends Controller {
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
const finalPriceWithVat = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const price = finalPriceWithVat;
const pricePerUnit = price / (1 + VAT_DEFAULT);
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
: 0;
return {
order: i + 1,
productId: v.productId,
@ -556,13 +549,13 @@ export class QuotationController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.vatExcluded =
c.vat === 0
? precisionRound(a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)))
: a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -663,14 +656,7 @@ export class QuotationController extends Controller {
title: "ใบเสนอราคาใหม่ / New Quotation",
detail: "รหัส / code : " + ret.code,
registeredBranchId: ret.registeredBranchId,
groupReceiver: {
create: [
{ name: "sale" },
{ name: "head_of_sale" },
{ name: "accountant" },
{ name: "branch_accountant" },
],
},
groupReceiver: { create: [{ name: "sale" }, { name: "head_of_sale" }] },
},
});
@ -678,7 +664,7 @@ export class QuotationController extends Controller {
}
@Put("{quotationId}")
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
@Security("keycloak", MANAGE_ROLES)
async editQuotation(
@Request() req: RequestWithUser,
@Path() quotationId: string,
@ -814,14 +800,14 @@ export class QuotationController extends Controller {
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
const finalPrice = precisionRound(
const finalPriceWithVat = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const price = finalPriceWithVat;
const pricePerUnit = price / (1 + VAT_DEFAULT);
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
: 0;
return {
@ -844,13 +830,15 @@ export class QuotationController extends Controller {
const price = list?.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -866,7 +854,6 @@ export class QuotationController extends Controller {
finalPrice: 0,
},
);
const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => {
return (
@ -900,20 +887,6 @@ export class QuotationController extends Controller {
}),
]);
if (customerBranch) {
await tx.customerBranch.update({
where: { id: customerBranch.id },
data: {
customer: {
update: {
status: Status.ACTIVE,
},
},
status: Status.ACTIVE,
},
});
}
return await tx.quotation.update({
include: {
productServiceList: {
@ -1035,7 +1008,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -1058,7 +1030,6 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;

View file

@ -95,6 +95,7 @@ export class RequestDataController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
@ -293,62 +294,34 @@ export class RequestDataController extends Controller {
async updateRequestData(
@Request() req: RequestWithUser,
@Body()
body: {
boby: {
defaultMessengerId: string;
requestDataId: string[];
},
) {
if (body.requestDataId.length === 0) return;
return await prisma.$transaction(async (tx) => {
const record = await tx.requestData.updateManyAndReturn({
where: {
id: { in: body.requestDataId },
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
const record = await prisma.requestData.updateManyAndReturn({
where: {
id: { in: boby.requestDataId },
quotation: {
registeredBranch: {
OR: permissionCond(req.user),
},
},
data: {
defaultMessengerId: body.defaultMessengerId,
},
});
if (record.length <= 0) throw notFoundError("Request Data");
await tx.requestWorkStepStatus.updateMany({
where: {
workStatus: {
in: [
RequestWorkStatus.Pending,
RequestWorkStatus.Waiting,
RequestWorkStatus.InProgress,
],
},
requestWork: {
requestDataId: { in: body.requestDataId },
},
},
data: { responsibleUserId: body.defaultMessengerId },
});
return record[0];
},
data: {
defaultMessengerId: boby.defaultMessengerId,
},
});
if (record.length <= 0) throw notFoundError("Request Data");
return record[0];
}
}
@Route("/api/v1/request-data/{requestDataId}")
@Tags("Request List")
export class RequestDataActionController extends Controller {
async #getLineToken() {
if (!process.env.LINE_MESSAGING_API_TOKEN) {
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
}
return process.env.LINE_MESSAGING_API_TOKEN;
}
@Post("reject-request-cancel")
@Security("keycloak")
async rejectRequestCancel(
@ -423,17 +396,6 @@ export class RequestDataActionController extends Controller {
},
},
},
include: {
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
},
},
});
if (!result) throw notFoundError("Request Data");
@ -476,88 +438,23 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder
.updateManyAndReturn({
where: {
taskList: {
every: { taskStatus: TaskStatus.Canceled },
},
tx.taskOrder.updateMany({
where: {
taskList: {
every: { taskStatus: TaskStatus.Canceled },
},
data: { taskOrderStatus: TaskStatus.Canceled },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
}),
]);
const token = await this.#getLineToken();
if (!token) return;
const textHead = "JWS ALERT:";
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
const textAlert2 = "ได้ดำเนินการยกเลิกเรียบร้อยแล้ว";
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
let finalTextWork = "";
let textData = "";
let dataCustomerId: string[] = [];
let dataUserId: string[] = [];
result.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `เลขที่ใบเสนอราคา: ${result.code} ${result.quotation.workName}`;
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
},
],
};
await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
data: { taskOrderStatus: TaskStatus.Canceled },
}),
]);
});
}
@ -689,19 +586,13 @@ export class RequestDataActionController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder.updateMany({
where: {
@ -784,83 +675,14 @@ export class RequestDataActionController extends Controller {
},
},
data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false },
include: {
customerBranch: {
include: {
customer: {
include: {
branch: {
where: { userId: { not: null } },
},
},
},
},
},
},
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
const token = await this.#getLineToken();
if (!token) return;
const textHead = "JWS ALERT:";
const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา";
const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว";
const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏";
let finalTextWork = "";
let textData = "";
let dataCustomerId: string[] = [];
let textWorkList: string[] = [];
let dataUserId: string[] = [];
if (res) {
res.forEach((data, index) => {
data.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`);
});
finalTextWork = textWorkList.join("\n");
}
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
},
],
};
await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
});
// dataRecord.push(record);
@ -1155,7 +977,7 @@ export class RequestListController extends Controller {
});
if (record.responsibleUserId === null) {
await tx.requestWorkStepStatus.update({
await prisma.requestWorkStepStatus.update({
where: {
step_requestWorkId: {
step: step,
@ -1217,19 +1039,13 @@ export class RequestListController extends Controller {
data: { quotationStatus: QuotationStatus.Canceled, urgent: false },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Canceled",
receiverId: v.createdByUserId,
})),
});
}),
tx.taskOrder.updateMany({
where: {
@ -1337,19 +1153,13 @@ export class RequestListController extends Controller {
},
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
const token = await this.#getLineToken();
if (!token) return;

View file

@ -44,21 +44,11 @@ import {
} from "../utils/minio";
import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"data_entry",
];
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin"];
return allowList.some((v) => user.roles?.includes(v));
}
const permissionCondCompany = createPermCondition((_) => true);
@ -70,14 +60,11 @@ const permissionCheckCompany = createPermCheck((_) => true);
@Tags("Task Order")
export class TaskController extends Controller {
@Get("stats")
@Security("keycloak")
async getTaskOrderStats(@Request() req: RequestWithUser) {
async getTaskOrderStats() {
const task = await prisma.taskOrder.groupBy({
where: { registeredBranch: { OR: permissionCondCompany(req.user) } },
by: ["taskOrderStatus"],
_count: true,
});
return task.reduce<Record<TaskOrderStatus, number>>(
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
{
@ -265,12 +252,6 @@ export class TaskController extends Controller {
taskProduct?: { productId: string; discount?: number }[];
},
) {
if (body.taskList.length < 1 || !body.registeredBranchId)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Your created invalid task order",
"taskOrderInvalid",
);
return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
where: {
@ -320,8 +301,8 @@ export class TaskController extends Controller {
if (updated.count !== taskList.length) {
throw new HttpError(
HttpStatus.PRECONDITION_FAILED,
"all request work to issue task order must be in ready state.",
"requestworkmustready",
"All request work to issue task order must be in ready state.",
"requestWorkMustReady",
);
}
await tx.institution.updateMany({
@ -344,51 +325,49 @@ export class TaskController extends Controller {
where: { OR: taskList },
});
return await tx.taskOrder
.create({
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
employee: true,
quotation: {
include: {
customerBranch: {
include: {
customer: true,
},
return await tx.taskOrder.create({
include: {
taskList: {
include: {
requestWorkStep: {
include: {
requestWork: {
include: {
request: {
include: {
employee: true,
quotation: {
include: {
customerBranch: {
include: {
customer: true,
},
},
},
},
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
productService: {
include: {
service: {
include: {
workflow: {
include: {
step: {
include: {
value: true,
responsiblePerson: {
include: { user: true },
},
responsibleInstitution: true,
},
},
},
},
},
work: true,
product: true,
},
work: true,
product: true,
},
},
},
@ -396,30 +375,20 @@ export class TaskController extends Controller {
},
},
},
institution: true,
createdBy: true,
},
data: {
...rest,
code,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
registeredBranchId: userAffiliatedBranch.id,
createdByUserId: req.user.sub,
taskList: { create: taskList },
taskProduct: { create: taskProduct },
},
})
.then(async (v) => {
await prisma.notification.create({
data: {
title: "ใบสั่งงานใหม่ / New Task Order",
detail: "รหัส / code : " + v.code,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
return v;
});
institution: true,
createdBy: true,
},
data: {
...rest,
code,
urgent: work.some((v) => v.requestWork.request.quotation.urgent),
registeredBranchId: userAffiliatedBranch.id,
createdByUserId: req.user.sub,
taskList: { create: taskList },
taskProduct: { create: taskProduct },
},
});
});
}
@ -562,8 +531,6 @@ export class TaskController extends Controller {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}
@ -639,28 +606,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: {
include: {
requestWork: {
include: {
request: {
include: {
quotation: true,
employee: true,
},
},
productService: {
include: {
product: true,
},
},
},
},
},
},
taskOrder: true,
},
include: { requestWorkStep: true },
where: {
step: v.step,
requestWorkId: v.requestWorkId,
@ -678,25 +624,6 @@ export class TaskActionController extends Controller {
data: { userTaskStatus: UserTaskStatus.Restart },
});
}
if (v.taskStatus === TaskStatus.Failed) {
const taskCode = record.taskOrder.code;
const taskName = record.taskOrder.taskName;
const productCode = record.requestWorkStep.requestWork.productService.product.code;
const productName = record.requestWorkStep.requestWork.productService.product.name;
const employeeName = `${record.requestWorkStep.requestWork.request.employee.namePrefix}.${record.requestWorkStep.requestWork.request.employee.firstNameEN} ${record.requestWorkStep.requestWork.request.employee.lastNameEN}`;
await tx.notification.create({
data: {
title: "ใบรายการคำขอที่จัดการเกิดปัญหา / Task Failed",
detail: `ใบรายการคำขอรหัส ${taskCode}: ${taskName} รหัสสินค้า ${productCode}: ${productName} ของลูกจ้าง ${employeeName} เกิดข้อผิดพลาด`,
groupReceiver: { create: { name: "document_checker" } },
receiverId: record.requestWorkStep.requestWork.request.quotation.createdByUserId,
registeredBranchId: record.taskOrder.registeredBranchId,
},
});
}
return await tx.task.update({
where: { id: record.id },
data: {
@ -758,8 +685,6 @@ export class TaskActionController extends Controller {
title: "มีการส่งงาน / Task Submitted",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
]);
@ -792,34 +717,22 @@ export class TaskActionController extends Controller {
const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`;
await Promise.all([
tx.taskOrder
.update({
where: { id: taskOrderId },
data: {
urgent: false,
taskOrderStatus: TaskOrderStatus.Complete,
codeProductReceived: code,
userTask: {
updateMany: {
where: { taskOrderId },
data: {
userTaskStatus: UserTaskStatus.Submit,
},
tx.taskOrder.update({
where: { id: taskOrderId },
data: {
urgent: false,
taskOrderStatus: TaskOrderStatus.Complete,
codeProductReceived: code,
userTask: {
updateMany: {
where: { taskOrderId },
data: {
userTaskStatus: UserTaskStatus.Submit,
},
},
},
})
.then(async (record) => {
await tx.notification.create({
data: {
title: "ใบงานเสร็จสิ้น / Task Complete",
detail: "รหัสใบสั่งงาน / Order : " + record.code,
receiverId: record.createdByUserId,
registeredBranchId: record.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
}),
},
}),
tx.requestWorkStepStatus.updateMany({
where: {
task: {
@ -923,34 +836,10 @@ export class TaskActionController extends Controller {
if (completeCheck) completed.push(item.id);
});
await tx.requestData
.updateManyAndReturn({
where: { id: { in: completed } },
include: {
quotation: {
select: {
registeredBranchId: true,
createdByUserId: true,
},
},
},
data: { requestDataStatus: RequestDataStatus.Completed },
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.quotation.createdByUserId,
registeredBranchId: v.quotation.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
});
await tx.requestData.updateMany({
where: { id: { in: completed } },
data: { requestDataStatus: RequestDataStatus.Completed },
});
await tx.quotation
.updateManyAndReturn({
where: {
@ -990,19 +879,13 @@ export class TaskActionController extends Controller {
},
})
.then(async (res) => {
await Promise.all(
res.map((v) =>
tx.notification.create({
data: {
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
}),
),
);
await tx.notification.createMany({
data: res.map((v) => ({
title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated",
detail: "รหัส / code : " + v.code + " Completed",
receiverId: v.createdByUserId,
})),
});
const token = await this.#getLineToken();
@ -1241,23 +1124,19 @@ export class UserTaskController extends Controller {
},
})
.then(async (v) => {
await tx.notification.create({
data: {
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
});
await tx.notification.create({
data: {
title: "มีการรับงาน / Task Accepted",
detail: "รหัสใบสั่งงาน / Order : " + v.code,
receiverId: v.createdByUserId,
registeredBranchId: v.registeredBranchId,
groupReceiver: { create: { name: "document_checker" } },
},
await tx.notification.createMany({
data: [
{
title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed",
detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress",
receiverId: v.createdByUserId,
},
{
title: "มีการรับงาน / Task Accepted",
detail: "รหัสใบสั่งงาน / Order : " + v.code,
receiverId: v.createdByUserId,
},
],
});
}),
tx.task.updateMany({

View file

@ -13,7 +13,6 @@ import {
Security,
Tags,
} from "tsoa";
import config from "../config.json";
import prisma from "../db";
@ -43,21 +42,22 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
const VAT_DEFAULT = config.vat;
// NOTE: permission condition/check in requestWork -> requestData -> quotation -> registeredBranch
const permissionCond = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type CreditNoteCreate = {
requestWorkId: string[];
@ -85,14 +85,6 @@ type CreditNoteUpdate = {
@Route("api/v1/credit-note")
@Tags("Credit Note")
export class CreditNoteController extends Controller {
async #getLineToken() {
if (!process.env.LINE_MESSAGING_API_TOKEN) {
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
}
return process.env.LINE_MESSAGING_API_TOKEN;
}
@Get("stats")
@Security("keycloak")
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
@ -102,7 +94,7 @@ export class CreditNoteController extends Controller {
request: {
quotationId,
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
@ -156,6 +148,7 @@ export class CreditNoteController extends Controller {
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: {},
) {
const where = {
OR: queryOrNot<Prisma.CreditNoteWhereInput[]>(query, [
@ -172,6 +165,7 @@ export class CreditNoteController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
@ -206,7 +200,7 @@ export class CreditNoteController extends Controller {
request: {
quotationId,
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
@ -217,8 +211,6 @@ export class CreditNoteController extends Controller {
const [result, total] = await prisma.$transaction([
prisma.creditNote.findMany({
where,
take: pageSize,
skip: (page - 1) * pageSize,
include: {
quotation: {
include: {
@ -251,7 +243,7 @@ export class CreditNoteController extends Controller {
some: {
request: {
quotation: {
registeredBranch: { OR: permissionCond(req.user) },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
},
},
@ -349,8 +341,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
c.productService.pricePerUnit -
c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
@ -376,98 +369,40 @@ export class CreditNoteController extends Controller {
update: { value: { increment: 1 } },
});
return await prisma.creditNote
.create({
include: {
requestWork: {
include: {
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
return await prisma.creditNote.create({
include: {
requestWork: {
include: {
request: true,
},
},
data: {
reason: body.reason,
detail: body.detail,
remark: body.remark,
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,
quotation: true,
},
data: {
reason: body.reason,
detail: body.detail,
remark: body.remark,
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,
})),
},
})
.then(async (res) => {
const token = await this.#getLineToken();
if (!token) return;
const textHead = "JWS ALERT:";
const textAlert = "ขอแจ้งให้ทราบว่าใบลดหนี้";
const textAlert2 = "ได้ถูกสร้างขึ้นเรียบร้อยแล้ว";
const textAlert3 =
"หากท่านต้องการข้อมูลเพิ่มเติมหรือมีข้อสงสัยประการใด โปรดแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ ทางเรายินดีให้ความช่วยเหลืออย่างเต็มที่ 🙏";
let finalTextWork = "";
let textData = "";
let dataCustomerId: string[] = [];
let dataUserId: string[] = [];
if (res) {
res.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
}
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
},
],
};
await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
return res;
});
quotationId: body.quotationId,
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
);
}
@Put("{creditNoteId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async updateCreditNote(
@Request() req: RequestWithUser,
@Path() creditNoteId: string,
@ -542,8 +477,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
c.productService.pricePerUnit -
c.productService.discount / c.productService.amount +
c.productService.vat / c.productService.amount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
@ -640,14 +576,6 @@ export class CreditNoteActionController extends Controller {
return creditNoteData;
}
async #getLineToken() {
if (!process.env.LINE_MESSAGING_API_TOKEN) {
console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set.");
}
return process.env.LINE_MESSAGING_API_TOKEN;
}
@Post("accept")
@Security("keycloak", MANAGE_ROLES)
async acceptCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
@ -666,81 +594,23 @@ export class CreditNoteActionController extends Controller {
@Body() body: { paybackStatus: PaybackStatus },
) {
await this.#checkPermission(req.user, creditNoteId);
return await prisma.creditNote
.update({
where: { id: creditNoteId },
include: {
requestWork: {
include: {
request: true,
},
},
quotation: {
include: {
customerBranch: {
include: {
customer: { include: { branch: { where: { userId: { not: null } } } } },
},
},
},
return await prisma.creditNote.update({
where: { id: creditNoteId },
include: {
requestWork: {
include: {
request: true,
},
},
data: {
creditNoteStatus:
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
paybackStatus: body.paybackStatus,
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
},
})
.then(async (res) => {
const token = await this.#getLineToken();
if (!token) return;
const textHead = "JWS ALERT:";
const textAlert = "ทางเราขอแจ้งให้ทราบว่าการดำเนินการคืนเงินสำหรับใบลดหนี้";
const textAlert2 = "ได้รับการอนุมัติและเสร็จสมบูรณ์เรียบร้อยแล้ว";
const textAlert3 =
"หากท่านต้องการข้อมูลเพิ่มเติมหรือมีข้อสงสัยประการใด โปรดแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ ทางเรายินดีให้ความช่วยเหลืออย่างเต็มที่ 🙏";
let finalTextWork = "";
let textData = "";
let dataCustomerId: string[] = [];
let textWorkList: string[] = [];
let dataUserId: string[] = [];
if (res) {
res.quotation.customerBranch.customer.branch.forEach((item) => {
if (!dataCustomerId?.includes(item.id) && item.userId) {
dataCustomerId.push(item.id);
dataUserId.push(item.userId);
}
});
finalTextWork = `จำนวนเงิน ${res.value.toFixed(2)} บาท `;
}
textData = `${textHead}\n\n${textAlert}\n${finalTextWork}${textAlert2}\n\n${textAlert3}`;
const data = {
to: dataUserId,
messages: [
{
type: "text",
text: textData,
},
],
};
body.paybackStatus === PaybackStatus.Done
? await fetch("https://api.line.me/v2/bot/message/multicast", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
: undefined;
});
quotation: true,
},
data: {
creditNoteStatus:
body.paybackStatus === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
paybackStatus: body.paybackStatus,
paybackDate: body.paybackStatus === PaybackStatus.Done ? new Date() : undefined,
},
});
}
}

View file

@ -44,20 +44,22 @@ const MANAGE_ROLES = [
"system",
"head_of_admin",
"admin",
"executive",
"head_of_accountant",
"accountant",
"branch_admin",
"branch_manager",
"branch_accountant",
"head_of_sale",
"sale",
];
function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"];
return allowList.some((v) => user.roles?.includes(v));
}
// NOTE: permission condition/check in registeredBranch
const permissionCond = createPermCondition(globalAllow);
const permissionCondCompany = createPermCondition((_) => true);
const permissionCheck = createPermCheck(globalAllow);
const permissionCheckCompany = createPermCheck((_) => true);
type DebitNoteCreate = {
quotationId: string;
@ -74,7 +76,6 @@ type DebitNoteCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -110,7 +111,6 @@ type DebitNoteUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string | null;
namePrefix?: string;
firstName?: string;
firstNameEN: string;
@ -211,6 +211,7 @@ export class DebitNoteController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
@ -430,18 +431,12 @@ export class DebitNoteController 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 finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
const price = body.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
@ -464,13 +459,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@ -582,7 +579,7 @@ export class DebitNoteController extends Controller {
}
@Put("{debitNoteId}")
@Security("keycloak")
@Security("keycloak", MANAGE_ROLES)
async updateDebitNote(
@Request() req: RequestWithUser,
@Path() debitNoteId: string,
@ -606,7 +603,7 @@ export class DebitNoteController extends Controller {
if (!record) throw notFoundError("Debit Note");
await permissionCheck(req.user, record.registeredBranch);
await permissionCheckCompany(req.user, record.registeredBranch);
const { productServiceList: _productServiceList, ...rest } = body;
const ids = {
@ -677,18 +674,12 @@ export class DebitNoteController 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 finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
(1 + VAT_DEFAULT)) *
VAT_DEFAULT
const price = body.agentPrice ? p.agentPrice : p.price;
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
const vat = p.calcVat
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
VAT_DEFAULT *
(!v.discount ? v.amount : 1)
: 0;
return {
@ -711,13 +702,15 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
const vat = c.vat ? VAT_DEFAULT : 0;
const price = c.pricePerUnit * c.amount * (1 + vat) - c.discount;
a.totalPrice = precisionRound(a.totalPrice + price / (1 + vat) + c.discount);
a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.vatExcluded =
c.vat === 0
? precisionRound(
a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
)
: a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);

View file

@ -189,6 +189,7 @@ export class LineController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
@ -613,22 +614,39 @@ export class LineController extends Controller {
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
]),
OR:
query || pendingOnly
? [
...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
]) || []),
...(queryOrNot<Prisma.QuotationWhereInput[]>(!!pendingOnly, [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
]) || []),
]
: undefined,
isDebitNote: false,
code,
payCondition,
@ -650,22 +668,6 @@ export class LineController extends Controller {
},
}
: undefined,
AND: pendingOnly
? {
OR: [
{
requestData: {
some: {
requestDataStatus: "Pending",
},
},
},
{
requestData: { none: {} },
},
],
}
: undefined,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;

View file

@ -90,7 +90,7 @@ export class WebHookController extends Controller {
firstNameEN: true,
lastName: true,
lastNameEN: true,
registerName: true,
customerName: true,
customer: {
select: {
customerType: true,
@ -133,13 +133,13 @@ export class WebHookController extends Controller {
let textData = "";
if (dataEmployee.length > 0) {
const registerName =
dataEmployee[0]?.employee?.customerBranch?.registerName ?? "ไม่ระบุ";
const customerName =
dataEmployee[0]?.employee?.customerBranch?.customerName ?? "ไม่ระบุ";
const telephoneNo =
dataEmployee[0]?.employee?.customerBranch?.customer.registeredBranch.telephoneNo ??
"ไม่ระบุ";
const textEmployer = `เรียน คุณ${registerName}`;
const textEmployer = `เรียน คุณ${customerName}`;
const textAlert = "ขอแจ้งให้ทราบว่าหนังสือเดินทางของลูกจ้าง";
const textAlert2 = "และจำเป็นต้องดำเนินการต่ออายุในเร็ว ๆ นี้";
const textExpDate =

View file

@ -1,113 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import { Prisma } from "@prisma/client";
import { queryOrNot } from "../utils/relation";
import { notFoundError } from "../utils/error";
type BusinessTypePayload = {
name: string;
nameEN: string;
};
@Route("api/v1/business-type")
@Tags("Business Type")
export class businessTypeController extends Controller {
@Get()
@Security("keycloak")
async getList(
@Request() req: RequestWithUser,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: queryOrNot<Prisma.BusinessTypeWhereInput[]>(query, [
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
]),
} satisfies Prisma.BusinessTypeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.businessType.findMany({
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.businessType.count({ where }),
]);
return { result, page, pageSize, total };
}
@Post()
@Security("keycloak")
async createBusinessType(@Request() req: RequestWithUser, @Body() body: BusinessTypePayload) {
return await prisma.businessType.create({
data: {
...body,
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
});
}
@Get(":businessTypeId")
@Security("keycloak")
async getBusinessTypeById(@Path() businessTypeId: string) {
return await prisma.businessType.findUnique({
where: { id: businessTypeId },
});
}
@Put(":businessTypeId")
@Security("keycloak")
async updateBusinessType(
@Request() req: RequestWithUser,
@Path() businessTypeId: string,
@Body() body: BusinessTypePayload,
) {
return await prisma.$transaction(async (tx) => {
const record = await tx.businessType.findUnique({
where: { id: businessTypeId },
});
if (!record) throw notFoundError("BusinessType");
return await tx.businessType.update({
where: { id: businessTypeId },
data: {
...body,
updatedByUserId: req.user.sub,
},
});
});
}
@Delete(":businessTypeId")
@Security("keycloak")
async deleteBusinessType(@Path() businessTypeId: string) {
return await prisma.$transaction(async (tx) => {
const record = await tx.businessType.findUnique({
where: { id: businessTypeId },
});
if (!record) throw notFoundError("BusinessType");
return await tx.businessType.delete({
where: { id: businessTypeId },
});
});
}
}

View file

@ -1,25 +0,0 @@
import express from "express";
import { Controller, Get, Path, Request, Route } from "tsoa";
import { getFile } from "../utils/minio";
@Route("api/v1/troubleshooting")
export class TroubleshootingController extends Controller {
@Get()
async get(@Request() req: express.Request) {
return req.res?.redirect(await getFile(".troubleshooting/toc.json"));
}
@Get("{category}/assets/{name}")
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
return req.res?.redirect(await getFile(`.troubleshooting/${category}/assets/${name}`));
}
@Get("{category}/page/{page}")
async getContent(
@Request() req: express.Request,
@Path() category: string,
@Path() page: string,
) {
return req.res?.redirect(await getFile(`.troubleshooting/${category}/${page}.md`));
}
}

View file

@ -1,10 +1,6 @@
import prisma from "../db";
import config from "../config.json";
import { CustomerType, PayCondition } from "@prisma/client";
import { convertTemplate } from "../utils/string-template";
import { htmlToText } from "html-to-text";
import { JsonObject } from "@prisma/client/runtime/library";
import { precisionRound } from "../utils/arithmetic";
if (!process.env.FLOW_ACCOUNT_URL) throw new Error("Require FLOW_ACCOUNT_URL");
if (!process.env.FLOW_ACCOUNT_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID");
@ -236,29 +232,6 @@ const flowAccount = {
installments: true,
quotation: {
include: {
paySplit: true,
worker: {
select: {
employee: {
select: {
employeePassport: {
select: {
number: true,
},
orderBy: {
expireDate: "desc",
},
take: 1,
},
namePrefix: true,
firstName: true,
lastName: true,
firstNameEN: true,
lastNameEN: true,
},
},
},
},
registeredBranch: {
include: {
province: true,
@ -289,58 +262,19 @@ const flowAccount = {
const quotation = data.quotation;
const customer = quotation.customerBranch;
const summary = {
subTotal: 0,
discountAmount: 0,
vatableAmount: 0,
exemptAmount: 0,
vatAmount: 0,
grandTotal: 0,
};
const products = (
const product =
quotation.payCondition === PayCondition.BillFull ||
quotation.payCondition === PayCondition.Full
? quotation.productServiceList
: quotation.productServiceList.filter((lhs) =>
data.installments.some((rhs) => rhs.no === lhs.installmentNo),
)
).map((v) => {
// TODO: Use product's VAT field (not implemented) instead.
const VAT_RATE = VAT_DEFAULT;
summary.subTotal +=
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount;
summary.discountAmount += v.discount;
const total =
precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_RATE : 0))) * v.amount -
(v.discount ?? 0);
if (v.vat > 0) {
summary.vatableAmount += precisionRound(total / (1 + VAT_RATE));
summary.vatAmount += v.vat;
} else {
summary.exemptAmount += total;
}
summary.grandTotal += total;
return {
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
pricePerUnit: precisionRound(v.pricePerUnit),
quantity: v.amount,
discountAmount: v.discount,
vatRate: v.vat === 0 ? 0 : Math.round(VAT_RATE * 100),
total,
};
});
);
const payload = {
contactCode: customer.code,
contactName: customer.contactName || "-",
contactName:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
: customer.registerName) || "-",
contactAddress: [
customer.address,
!!customer.moo ? "หมู่ " + customer.moo : null,
@ -349,10 +283,11 @@ const flowAccount = {
(customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name,
(customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name,
"จังหวัด" + customer.province?.name,
customer.subDistrict?.zipCode,
]
.filter(Boolean)
.join(" "),
contactTaxId: customer.citizenId || customer.legalPersonNo || "-",
contactTaxId: customer.citizenId || customer.code,
contactBranch:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
@ -370,35 +305,36 @@ const flowAccount = {
isVat: true,
useReceiptDeduction: false,
useInlineVat: true,
discounPercentage: 0,
discountAmount: quotation.totalDiscount,
subTotal: summary.subTotal,
totalAfterDiscount: summary.subTotal - summary.discountAmount,
vatableAmount: summary.vatableAmount,
exemptAmount: summary.exemptAmount,
vatAmount: summary.vatAmount,
grandTotal: summary.grandTotal,
subTotal:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.totalPrice,
totalAfterDiscount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.finalPrice,
vatAmount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.vat,
grandTotal:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? data.installments.reduce((a, c) => a + c.amount, 0)
: quotation.finalPrice,
remarks: htmlToText(
convertTemplate(quotation.remark ?? "", {
"quotation-payment": {
paymentType: quotation?.payCondition || "Full",
amount: quotation.finalPrice,
installments: quotation?.paySplit,
},
"quotation-labor": {
name: quotation.worker.map(
(v, i) =>
`${i + 1}. ` +
`${v.employee.employeePassport.length !== 0 ? v.employee.employeePassport[0].number + "_" : ""}${v.employee.namePrefix}. ${v.employee.firstNameEN ? `${v.employee.firstNameEN} ${v.employee.lastNameEN}` : `${v.employee.firstName} ${v.employee.lastName}`} `.toUpperCase(),
),
},
}),
),
items: products,
items: product.map((v) => ({
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
pricePerUnit: v.pricePerUnit,
quantity: v.amount,
discountAmount: v.discount,
total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
})),
};
return await flowAccountAPI.createReceipt(payload, false);
@ -411,219 +347,6 @@ const flowAccount = {
}
return null;
},
// flowAccount GET Product list
async getProducts() {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + "/products", {
method: "GET",
headers: {
["Content-Type"]: `application/json`,
["Authorization"]: `Bearer ${token}`,
},
});
return {
ok: res.ok,
status: res.status,
body: await res.json(),
};
},
// flowAccount GET Product by id
async getProductsById(recordId: string) {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + `/products/${recordId}`, {
method: "GET",
headers: {
["Content-Type"]: `application/json`,
["Authorization"]: `Bearer ${token}`,
},
});
const data = await res.json();
return {
ok: res.ok,
status: res.status,
list: data.data.list,
total: data.data.total,
};
},
// flowAccount POST create Product
async createProducts(code: string, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
productStructureType: null,
type: 3,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
buyPrice: body.serviceCharge,
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
buyDescription: body.detail,
};
const createProduct = async (name: string, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(`${api}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...commonBody,
name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: Failed to create product`);
const json = await res.json().catch(() => {
throw new Error("Invalid JSON response from FlowAccount API");
});
return json?.data?.list?.[0]?.id ?? null;
} catch (err) {
console.error("createProduct error:", err);
return null;
}
};
const deleteProduct = async (id: string) => {
try {
await fetch(`${api}/products/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
} catch (err) {
console.error("Rollback delete failed:", err);
}
};
const [sellResult, agentResult] = await Promise.allSettled([
createProduct(`${code} ${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
createProduct(
`${code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
]);
const sellId = sellResult.status === "fulfilled" ? sellResult.value : null;
const agentId = agentResult.status === "fulfilled" ? agentResult.value : null;
// --- validation ---
if (!sellId && !agentId) {
throw new Error("FlowAccountProductError.BOTH_CREATION_FAILED");
}
if (!sellId && agentId) {
await deleteProduct(agentId);
throw new Error("FlowAccountProductError.SELL_PRICE_CREATION_FAILED");
}
if (sellId && !agentId) {
await deleteProduct(sellId);
throw new Error("FlowAccountProductError.AGENT_PRICE_CREATION_FAILED");
}
return {
ok: true,
status: 200,
data: {
productIdSellPrice: sellId,
productIdAgentPrice: agentId,
},
};
},
// flowAccount PUT edit Product
async editProducts(sellPriceId: String, agentPriceId: String, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
productStructureType: null,
type: 3,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
buyPrice: body.serviceCharge,
buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
buyDescription: body.detail,
};
const editProduct = async (id: String, name: String, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(api + `/products/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...commonBody,
name: name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) {
throw new Error(`Request failed with status ${res.status} ${res}`);
}
let json: any = null;
try {
json = await res.json();
} catch {
throw new Error("Response is not valid JSON");
}
return json?.data?.list?.[0]?.id ?? null;
} catch (err) {
console.error("createProduct error:", err);
return null;
}
};
await Promise.all([
editProduct(
sellPriceId,
`${body.code} ${body.name}`,
body.price,
/true/.test(`${body.vatIncluded}`),
),
editProduct(
agentPriceId,
`${body.code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
]);
},
// flowAccount DELETE Product
async deleteProduct(recordId: string) {
const { token } = await flowAccountAPI.auth();
const res = await fetch(api + `/products/${recordId}`, {
method: "DELETE",
headers: {
["Authorization"]: `Bearer ${token}`,
},
});
return {
ok: res.ok,
status: res.status,
};
},
};
export default flowAccount;

View file

@ -358,9 +358,9 @@ export async function getGroup(query: string) {
const dataMainGroup = await res.json();
const fetchSubGroups = async (group: any) => {
let fullSubGroup = await Promise.all(
group.subGroups.map((subGroupsData: any) => {
group.subGroups.map(async (subGroupsData: any) => {
if (group.subGroupCount > 0) {
return fetchSubGroups(subGroupsData);
return await fetchSubGroups(subGroupsData);
} else {
return {
id: subGroupsData.id,

View file

@ -2,7 +2,6 @@ import dayjs from "dayjs";
import { CronJob } from "cron";
import prisma from "../db";
import { Prisma } from "@prisma/client";
const jobs = [
CronJob.from({
@ -39,162 +38,6 @@ const jobs = [
.catch((e) => console.error("[ERR]: Update expired quotation status, FAILED.", e));
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeExpireData = await prisma.employee.findMany({
include: {
employeePassport: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeePassport: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeExpireData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeePassport[0].expireDate).format("DD/MM")}/${dayjs(record.employeePassport[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} หนังสือเดินทางจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "หนังสือเดินทางลูกจ้างหมดอายุ / Employee Passport Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee passport expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee passport expired, FAILED.", e),
);
}
}),
);
},
}),
CronJob.from({
cronTime: "0 0 0 * * *",
runOnInit: true,
onTick: async () => {
const employeeVisaData = await prisma.employee.findMany({
include: {
employeeVisa: {
orderBy: {
expireDate: "desc",
},
take: 1,
},
customerBranch: {
include: {
customer: true,
},
},
quotationWorker: {
include: {
quotation: true,
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
where: {
employeeVisa: {
some: {
expireDate: dayjs().add(90, "day").toDate(),
},
},
},
});
await Promise.all(
employeeVisaData.map(async (record) => {
const fullName = `${record.namePrefix}.${record.firstNameEN} ${record.lastNameEN}`;
const expireDate = `${dayjs(record.employeeVisa[0].expireDate).format("DD/MM")}/${dayjs(record.employeeVisa[0].expireDate).year() + 543}`;
const textDetail = `ลูกจ้างรหัส / code : ${record.code} ชื่อ : ${fullName} ข้อมูลการตรวจลงตราจะหมดอายุในวันที่ ${expireDate}`;
const duplicateText = await prisma.notification.findFirst({
where: {
detail: textDetail,
},
});
const dataNotification: Prisma.NotificationCreateArgs["data"] = {
title: "ข้อมูลการตรวจลงตราลูกจ้างหมดอายุ / Employee Visa Expire",
detail: textDetail,
};
if (record.quotationWorker && record.quotationWorker.length > 0) {
dataNotification.receiverId = record.quotationWorker[0].quotation.updatedByUserId;
dataNotification.registeredBranchId =
record.quotationWorker[0].quotation.registeredBranchId;
} else {
(dataNotification.groupReceiver = {
create: [{ name: "sale" }, { name: "head_of_sale" }],
}),
(dataNotification.registeredBranchId =
record.customerBranch.customer.registeredBranchId);
}
if (!duplicateText) {
await prisma.notification
.create({
data: dataNotification,
})
.then(() => console.log("[INFO]: Create notification employee visa expired, OK."))
.catch((e) =>
console.error("[ERR]: Create notification employee visa expired, FAILED.", e),
);
}
}),
);
},
}),
];
export function initSchedule() {

View file

@ -1,67 +0,0 @@
export function formatNumberDecimal(num: number, point: number = 2): string {
return (num || 0).toLocaleString("eng", {
minimumFractionDigits: point,
maximumFractionDigits: point,
});
}
const templates = {
"quotation-labor": {
converter: (context?: { name: string[] }) => {
return context?.name.join("<br />") || "";
},
},
"quotation-payment": {
converter: (context?: {
paymentType: "Full" | "Split" | "SplitCustom" | "BillFull" | "BillSplit" | "BillSplitCustom";
amount?: number;
installments?: {
no: number;
amount: number;
}[];
}) => {
if (context?.paymentType === "Full") {
return [
"**** เงื่อนไขเพิ่มเติม",
"- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
`&nbsp; จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
].join("<br/>");
} else {
return [
"**** เงื่อนไขเพิ่มเติม",
`- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
...(context?.installments?.map(
(v) => `&nbsp; งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
) || []),
].join("<br />");
}
},
},
} as const;
type Template = typeof templates;
type TemplateName = keyof Template;
type TemplateContext = {
[key in TemplateName]?: Parameters<Template[key]["converter"]>[0];
};
export function convertTemplate(
text: string,
context?: TemplateContext,
templateUse?: TemplateName[],
) {
let ret = text;
for (const [name, template] of Object.entries(templates)) {
if (templateUse && !templateUse.includes(name as TemplateName)) continue;
ret = ret.replace(
new RegExp("\\#\\[" + name.replaceAll("-", "\\-") + "\\]", "g"),
typeof template.converter === "function"
? template.converter(context?.[name as TemplateName] as any)
: template.converter,
);
}
return ret;
}

View file

@ -62,90 +62,85 @@ export async function initThailandAreaDatabase() {
return result;
}
await prisma.$transaction(
async (tx) => {
const meta = {
createdBy: null,
createdAt: new Date(),
updatedBy: null,
updatedAt: new Date(),
};
await prisma.$transaction(async (tx) => {
const meta = {
createdBy: null,
createdAt: new Date(),
updatedBy: null,
updatedAt: new Date(),
};
await Promise.all(
splitChunk(province, 1000, async (r) => {
return await tx.$kysely
.insertInto("Province")
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(province, 1000, async (r) => {
return await tx.$kysely
.insertInto("Province")
.columns(["id", "name", "nameEN", "createdBy", "createdAt", "updatedBy", "updatedAt"])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(district, 2000, async (r) => {
return await tx.$kysely
.insertInto("District")
.columns([
"id",
"name",
"nameEN",
"provinceId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
provinceId: (eb) => eb.ref("excluded.provinceId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(district, 2000, async (r) => {
return await tx.$kysely
.insertInto("District")
.columns([
"id",
"name",
"nameEN",
"provinceId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
provinceId: (eb) => eb.ref("excluded.provinceId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
await Promise.all(
splitChunk(subDistrict, 1000, async (r) => {
return await tx.$kysely
.insertInto("SubDistrict")
.columns([
"id",
"name",
"nameEN",
"districtId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
districtId: (eb) => eb.ref("excluded.districtId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
},
{
timeout: 15_000,
},
);
await Promise.all(
splitChunk(subDistrict, 1000, async (r) => {
return await tx.$kysely
.insertInto("SubDistrict")
.columns([
"id",
"name",
"nameEN",
"districtId",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
])
.values(r.map((v) => ({ ...v, ...meta })))
.onConflict((oc) =>
oc.column("id").doUpdateSet({
name: (eb) => eb.ref("excluded.name"),
nameEN: (eb) => eb.ref("excluded.nameEN"),
districtId: (eb) => eb.ref("excluded.districtId"),
updatedAt: (eb) => eb.ref("excluded.updatedAt"),
}),
)
.execute();
}),
);
});
console.log("[INFO]: Sync thailand province, district and subdistrict, OK.");
}
@ -175,72 +170,67 @@ export async function initEmploymentOffice() {
const list = await prisma.province.findMany();
await prisma.$transaction(
async (tx) => {
await Promise.all(
list
.map(async (province) => {
if (special[province.id]) {
await tx.employmentOffice.deleteMany({
where: { provinceId: province.id, district: { none: {} } },
});
return await Promise.all(
Object.entries(special[province.id]).map(async ([key, val]) => {
const id = province.id + "-" + key.padStart(2, "0");
return tx.employmentOffice.upsert({
where: { id },
create: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
update: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
deleteMany: { districtId: { notIn: val } },
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
});
}),
);
}
return tx.employmentOffice.upsert({
where: { id: province.id },
create: {
id: province.id,
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
update: {
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
await prisma.$transaction(async (tx) => {
await Promise.all(
list
.map(async (province) => {
if (special[province.id]) {
await tx.employmentOffice.deleteMany({
where: { provinceId: province.id, district: { none: {} } },
});
})
.flat(),
);
},
{
timeout: 15_000,
},
);
return await Promise.all(
Object.entries(special[province.id]).map(async ([key, val]) => {
const id = province.id + "-" + key.padStart(2, "0");
return tx.employmentOffice.upsert({
where: { id },
create: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
update: {
id,
name: nameSpecial(province.name, +key),
nameEN: nameSpecialEN(province.nameEN, +key),
provinceId: province.id,
district: {
deleteMany: { districtId: { notIn: val } },
createMany: {
data: val.map((districtId) => ({ districtId })),
skipDuplicates: true,
},
},
},
});
}),
);
}
return tx.employmentOffice.upsert({
where: { id: province.id },
create: {
id: province.id,
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
update: {
name: name(province.name),
nameEN: nameEN(province.nameEN),
provinceId: province.id,
},
});
})
.flat(),
);
});
console.log("[INFO]: Sync employment office, OK.");
}

View file

@ -1,323 +0,0 @@
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from "vitest";
import { PrismaClient } from "@prisma/client";
import { isDateString } from "./lib";
const prisma = new PrismaClient({
datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL,
});
const baseUrl = process.env.TEST_BASE_URL || "http://localhost";
const record: Record<string, any> = {
code: "CMT",
taxNo: "1052299402851",
name: "Chamomind",
nameEN: "Chamomind",
email: "contact@chamomind.com",
lineId: "@chamomind",
telephoneNo: "0988929248",
contactName: "John",
webUrl: "https://chamomind.com",
latitude: "",
longitude: "",
virtual: false,
permitNo: "1135182804792",
permitIssueDate: "2025-01-01T00:00:00.000Z",
permitExpireDate: "2030-01-01T00:00:00.000Z",
address: "11/3",
addressEN: "11/3",
soi: "1",
soiEN: "1",
moo: "2",
mooEN: "2",
street: "Straight",
streetEN: "Straight",
provinceId: "50",
districtId: "5001",
subDistrictId: "500107",
};
const recordList: Record<string, any>[] = [];
let token: string;
beforeAll(async () => {
const body = new URLSearchParams();
body.append("grant_type", "password");
body.append("client_id", "app");
body.append("username", process.env.TEST_USERNAME || "");
body.append("password", process.env.TEST_PASSWORD || "");
body.append("scope", "openid");
const res = await fetch(
process.env.KC_URL + "/realms/" + process.env.KC_REALM + "/protocol/openid-connect/token",
{
method: "POST",
body: body,
},
);
expect(res.ok).toBe(true);
await res.json().then((data) => {
token = data["access_token"];
});
});
afterAll(async () => {
if (!record["id"]) return;
await prisma.branch.deleteMany({
where: { id: { in: [record, ...recordList].map((v) => v["id"]) } },
});
await prisma.runningNo.deleteMany({
where: {
key: { in: [record, ...recordList].map((v) => `MAIN_BRANCH_${v["code"].slice(0, -5)}`) },
},
});
});
describe("branch management", () => {
it("create branch without required fields", async () => {
const requiredFields = [
"taxNo",
"name",
"nameEN",
"permitNo",
"telephoneNo",
"address",
"addressEN",
"email",
];
onTestFailed(() => console.log("Field:", requiredFields, "is required."));
for await (const field of requiredFields) {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "POST",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify({ ...record, [field]: undefined }),
});
if (res.ok) recordList.push(await res.json());
expect(res.ok).toBe(false);
expect(res.status).toBe(400);
}
});
it("create branch", async () => {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "POST",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify(record),
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
record["id"] = data["id"]; // This field is auto generated
record["code"] = data["code"]; // This field is auto generated
recordList.push(data);
expect(data).toMatchObject(record);
});
it("get branch list", async () => {
const res = await fetch(baseUrl + "/api/v1/branch", {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toHaveProperty("result");
expect(data).toHaveProperty("total");
expect(data).toHaveProperty("page");
expect(data).toHaveProperty("pageSize");
expect(data.result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
code: expect.any(String),
virtual: expect.any(Boolean),
name: expect.any(String),
nameEN: expect.any(String),
email: expect.any(String),
taxNo: expect.any(String),
telephoneNo: expect.any(String),
latitude: expect.any(String),
longitude: expect.any(String),
contactName: expect.toBeOneOf([expect.any(String), null]),
lineId: expect.toBeOneOf([expect.any(String), null]),
webUrl: expect.toBeOneOf([expect.any(String), null]),
remark: expect.toBeOneOf([expect.any(String), null]),
selectedImage: expect.toBeOneOf([expect.any(String), null]),
isHeadOffice: expect.any(Boolean),
permitNo: expect.any(String),
permitIssueDate: expect.toSatisfy(isDateString(true)),
permitExpireDate: expect.toSatisfy(isDateString(true)),
address: expect.any(String),
addressEN: expect.any(String),
moo: expect.toBeOneOf([expect.any(String), null]),
mooEN: expect.toBeOneOf([expect.any(String), null]),
street: expect.toBeOneOf([expect.any(String), null]),
streetEN: expect.toBeOneOf([expect.any(String), null]),
provinceId: expect.any(String),
province: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
}),
districtId: expect.any(String),
district: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
}),
subDistrictId: expect.any(String),
subDistrict: expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
nameEN: expect.any(String),
zipCode: expect.any(String),
}),
status: expect.toBeOneOf(["CREATED", "ACTIVE", "INACTIVE"]),
statusOrder: expect.toBeOneOf([1, 0]),
createdAt: expect.toSatisfy(isDateString()),
createdByUserId: expect.toBeOneOf([expect.any(String), null]),
createdBy: expect.objectContaining({
id: expect.any(String),
username: expect.any(String),
firstName: expect.any(String),
lastName: expect.any(String),
firstNameEN: expect.any(String),
lastNameEN: expect.any(String),
}),
updatedAt: expect.toSatisfy(isDateString()),
updatedByUserId: expect.toBeOneOf([expect.any(String), null]),
updatedBy: expect.objectContaining({
id: expect.any(String),
username: expect.any(String),
firstName: expect.any(String),
lastName: expect.any(String),
firstNameEN: expect.any(String),
lastNameEN: expect.any(String),
}),
_count: expect.objectContaining({
branch: expect.any(Number),
}),
}),
]),
);
});
it("get branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("update branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "PUT",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
body: JSON.stringify({ name: "Chamomind Intl.", nameEN: "Chamomind Intl." }),
});
record["name"] = "Chamomind Intl.";
record["nameEN"] = "Chamomind Intl.";
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("delete branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "DELETE",
headers: {
["Content-Type"]: "application/json",
["Authorization"]: "Bearer " + token,
},
});
if (!res.ok) {
const text = await res.text();
try {
console.log(JSON.parse(text));
} catch (e) {
console.log(text);
}
}
expect(res.ok).toBe(true);
const data = await res.json();
expect(data).toMatchObject(record);
});
it("get deleted branch by id", async () => {
const res = await fetch(baseUrl + "/api/v1/branch/" + record["id"], {
method: "GET",
headers: {
["Authorization"]: "Bearer " + token,
},
});
expect(res.ok).toBe(false);
});
});

View file

@ -1,10 +0,0 @@
export function isDateString(nullable: boolean = false): (val: any) => boolean {
return (value: any) => {
try {
if (value) return !!new Date(value);
return nullable;
} catch (_) {
return false;
}
};
}

View file

@ -1,5 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {},
});