From d95eb349ec62f6c5dc05e94dac478213af790206 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Fri, 22 Aug 2025 14:23:40 +0700
Subject: [PATCH 01/54] fix: update messenger also update work step messenger
---
src/controllers/06-request-list-controller.ts | 50 +++++++++++++------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts
index 764fbc7..a453716 100644
--- a/src/controllers/06-request-list-controller.ts
+++ b/src/controllers/06-request-list-controller.ts
@@ -293,28 +293,48 @@ export class RequestDataController extends Controller {
async updateRequestData(
@Request() req: RequestWithUser,
@Body()
- boby: {
+ body: {
defaultMessengerId: string;
requestDataId: string[];
},
) {
- const record = await prisma.requestData.updateManyAndReturn({
- where: {
- id: { in: boby.requestDataId },
- quotation: {
- registeredBranch: {
- OR: permissionCond(req.user),
+ 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),
+ },
},
},
- },
- data: {
- defaultMessengerId: boby.defaultMessengerId,
- },
+ 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];
});
-
- if (record.length <= 0) throw notFoundError("Request Data");
-
- return record[0];
}
}
From ab8fd2ca4337cbbeaf0736b4eb8cdd8da4806c47 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 28 Aug 2025 14:38:49 +0700
Subject: [PATCH 02/54] feat: export customer and employee as csv
---
src/controllers/03-customer-controller.ts | 42 ++++++++++++++++++
src/controllers/03-employee-controller.ts | 54 ++++++++++++++++++++++-
2 files changed, 95 insertions(+), 1 deletion(-)
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 2f31d79..7e16f2d 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -37,6 +37,7 @@ 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",
@@ -547,3 +548,44 @@ 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,
+ ) {
+ const ret = await this.list(
+ req,
+ customerType,
+ query,
+ status,
+ page,
+ pageSize,
+ includeBranch,
+ company,
+ activeBranchOnly,
+ startDate,
+ endDate,
+ );
+
+ 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 },
+ );
+ }
+}
diff --git a/src/controllers/03-employee-controller.ts b/src/controllers/03-employee-controller.ts
index 411b817..303ba15 100644
--- a/src/controllers/03-employee-controller.ts
+++ b/src/controllers/03-employee-controller.ts
@@ -42,6 +42,7 @@ import {
listFile,
setFile,
} from "../utils/minio";
+import { json2csv } from "json-2-csv";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
@@ -249,7 +250,6 @@ export class EmployeeController extends Controller {
endDate,
);
}
-
@Post("list")
@Security("keycloak")
async listByCriteria(
@@ -927,3 +927,55 @@ 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 },
+ );
+ }
+}
From f487a9169ca147695bdbbea312e633d90a17a9f4 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 28 Aug 2025 15:01:04 +0700
Subject: [PATCH 03/54] fix: prevent line user id and otp exposes
---
src/controllers/00-doc-template-controller.ts | 5 +++
.../03-customer-branch-controller.ts | 15 +++++++++
src/controllers/03-customer-controller.ts | 33 ++++++++++++++++++-
3 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/src/controllers/00-doc-template-controller.ts b/src/controllers/00-doc-template-controller.ts
index da5dc9d..09dab18 100644
--- a/src/controllers/00-doc-template-controller.ts
+++ b/src/controllers/00-doc-template-controller.ts
@@ -34,6 +34,11 @@ const quotationData = (id: string) =>
},
},
customerBranch: {
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
include: {
customer: true,
businessType: true,
diff --git a/src/controllers/03-customer-branch-controller.ts b/src/controllers/03-customer-branch-controller.ts
index eda1546..a4d87e4 100644
--- a/src/controllers/03-customer-branch-controller.ts
+++ b/src/controllers/03-customer-branch-controller.ts
@@ -238,6 +238,11 @@ 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,
@@ -262,6 +267,11 @@ 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,
@@ -352,6 +362,11 @@ export class CustomerBranchController extends Controller {
include: branchRelationPermInclude(req.user),
},
branch: {
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
take: 1,
orderBy: { createdAt: "asc" },
},
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 7e16f2d..1854842 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -207,6 +207,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
}
: {
@@ -215,6 +220,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
take: 1,
orderBy: { createdAt: "asc" },
},
@@ -245,6 +255,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
orderBy: { createdAt: "asc" },
},
createdBy: true,
@@ -316,6 +331,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
},
createdBy: true,
updatedBy: true,
@@ -415,6 +435,11 @@ export class CustomerController extends Controller {
district: true,
subDistrict: true,
},
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
},
createdBy: true,
updatedBy: true,
@@ -453,7 +478,13 @@ export class CustomerController extends Controller {
await deleteFolder(`customer/${customerId}`);
const data = await tx.customer.delete({
include: {
- branch: true,
+ branch: {
+ omit: {
+ otpCode: true,
+ otpExpires: true,
+ userId: true,
+ },
+ },
registeredBranch: {
include: {
headOffice: true,
From 4042cbcea4b5b92a5256a9af48640b3197f2d467 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Mon, 1 Sep 2025 09:27:53 +0700
Subject: [PATCH 04/54] chore: add html to text dep
---
package.json | 2 +
pnpm-lock.yaml | 109 +++++++++++++++++++++++++++++++++++
src/utils/string-template.ts | 67 +++++++++++++++++++++
3 files changed, 178 insertions(+)
create mode 100644 src/utils/string-template.ts
diff --git a/package.json b/package.json
index 2f64c4f..13cd0e0 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@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",
"cors": "^2.8.5",
"cron": "^3.3.1",
@@ -53,6 +54,7 @@
"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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8471a8c..8d3df75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
'@tsoa/runtime':
specifier: ^6.6.0
version: 6.6.0
+ '@types/html-to-text':
+ specifier: ^9.0.4
+ version: 9.0.4
canvas:
specifier: ^3.1.0
version: 3.1.0
@@ -53,6 +56,9 @@ importers:
fast-jwt:
specifier: ^5.0.5
version: 5.0.5
+ html-to-text:
+ specifier: ^9.0.5
+ version: 9.0.5
jsbarcode:
specifier: ^3.11.6
version: 3.11.6
@@ -676,6 +682,9 @@ packages:
resolution: {integrity: sha512-4mQYkQJO0HHaoFd8Z+vSdQAvYcCJ2bRLN9ewE+GneB8kvoLG/oM3ynroqzGQdoytH8BmhnJwD3aEUagfbK2x5g==}
engines: {node: '>=18'}
+ '@selderee/plugin-htmlparser2@0.11.0':
+ resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -742,6 +751,9 @@ packages:
'@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
+ '@types/html-to-text@9.0.4':
+ resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==}
+
'@types/http-assert@1.5.6':
resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==}
@@ -1307,6 +1319,10 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1347,6 +1363,19 @@ packages:
resolution: {integrity: sha512-tTmR3WhROYctuyVReQ+PfCU3zprmC45/VuSVzn8EjovzpRkXYUdXiDatB9M8pasj0V+wuuOyY8bcSHvlQ2GNag==}
engines: {node: '>=6'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
@@ -1398,6 +1427,10 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -1740,6 +1773,13 @@ packages:
resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==}
engines: {node: '>=14'}
+ html-to-text@9.0.5:
+ resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
+ engines: {node: '>=14'}
+
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -2040,6 +2080,9 @@ packages:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
+ leac@0.6.0:
+ resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
+
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
@@ -2467,6 +2510,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
+ parseley@0.12.1:
+ resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -2504,6 +2550,9 @@ packages:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
+ peberminta@0.9.0:
+ resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2746,6 +2795,9 @@ packages:
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+ selderee@0.11.0:
+ resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
+
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -4037,6 +4089,11 @@ snapshots:
'@scalar/openapi-types': 0.1.7
'@unhead/schema': 1.11.14
+ '@selderee/plugin-htmlparser2@0.11.0':
+ dependencies:
+ domhandler: 5.0.3
+ selderee: 0.11.0
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -4136,6 +4193,8 @@ snapshots:
'@types/qs': 6.9.17
'@types/serve-static': 1.15.7
+ '@types/html-to-text@9.0.4': {}
+
'@types/http-assert@1.5.6': {}
'@types/http-errors@2.0.4': {}
@@ -4781,6 +4840,8 @@ snapshots:
deep-extend@0.6.0: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -4823,6 +4884,24 @@ snapshots:
jszip: 3.10.1
sax: 1.3.0
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dotenv@16.0.3: {}
dotenv@16.4.7: {}
@@ -4908,6 +4987,8 @@ snapshots:
dependencies:
once: 1.4.0
+ entities@4.5.0: {}
+
env-paths@2.2.1: {}
error-callsites@2.0.4:
@@ -5399,6 +5480,21 @@ snapshots:
hpagent@1.2.0: {}
+ html-to-text@9.0.5:
+ dependencies:
+ '@selderee/plugin-htmlparser2': 0.11.0
+ deepmerge: 4.3.1
+ dom-serializer: 2.0.0
+ htmlparser2: 8.0.2
+ selderee: 0.11.0
+
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 4.5.0
+
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -5689,6 +5785,8 @@ snapshots:
dependencies:
readable-stream: 2.3.8
+ leac@0.6.0: {}
+
lie@3.3.0:
dependencies:
immediate: 3.0.6
@@ -6096,6 +6194,11 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
+ parseley@0.12.1:
+ dependencies:
+ leac: 0.6.0
+ peberminta: 0.9.0
+
parseurl@1.3.3: {}
path-exists@4.0.0: {}
@@ -6119,6 +6222,8 @@ snapshots:
pathval@2.0.0: {}
+ peberminta@0.9.0: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -6428,6 +6533,10 @@ snapshots:
secure-json-parse@2.7.0: {}
+ selderee@0.11.0:
+ dependencies:
+ parseley: 0.12.1
+
semver@5.7.2: {}
semver@6.3.1: {}
diff --git a/src/utils/string-template.ts b/src/utils/string-template.ts
new file mode 100644
index 0000000..aaa477e
--- /dev/null
+++ b/src/utils/string-template.ts
@@ -0,0 +1,67 @@
+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("
") || "";
+ },
+ },
+ "quotation-payment": {
+ converter: (context?: {
+ paymentType: "Full" | "Split" | "SplitCustom" | "BillFull" | "BillSplit" | "BillSplitCustom";
+
+ amount?: number;
+ installments?: {
+ no: number;
+ amount: number;
+ }[];
+ }) => {
+ if (context?.paymentType === "Full") {
+ return [
+ "**** เงื่อนไขเพิ่มเติม",
+ "- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
+ ` จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
+ ].join("
");
+ } else {
+ return [
+ "**** เงื่อนไขเพิ่มเติม",
+ `- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
+ ...(context?.installments?.map(
+ (v) => ` งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
+ ) || []),
+ ].join("
");
+ }
+ },
+ },
+} as const;
+
+type Template = typeof templates;
+type TemplateName = keyof Template;
+type TemplateContext = {
+ [key in TemplateName]?: Parameters[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;
+}
From 710382d5441eb6c36f543d9d1174a6109b73e8c4 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 09:17:23 +0700
Subject: [PATCH 05/54] feat: add more metadata for payment
---
prisma/schema.prisma | 7 +++++--
src/controllers/05-payment-controller.ts | 10 +++++++++-
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 00bd192..78609d6 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1524,8 +1524,11 @@ model Payment {
paymentStatus PaymentStatus
- amount Float
- date DateTime?
+ amount Float
+ date DateTime?
+ channel String?
+ account String?
+ reference String?
createdAt DateTime @default(now())
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts
index 5f530d0..c61d4bc 100644
--- a/src/controllers/05-payment-controller.ts
+++ b/src/controllers/05-payment-controller.ts
@@ -113,7 +113,15 @@ export class QuotationPayment extends Controller {
@Security("keycloak", MANAGE_ROLES.concat(["head_of_sale", "sale"]))
async updatePayment(
@Path() paymentId: string,
- @Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
+ @Body()
+ body: {
+ amount?: number;
+ date?: Date;
+ paymentStatus?: PaymentStatus;
+ channel?: string | null;
+ account?: string | null;
+ reference?: string | null;
+ },
@Request() req: RequestWithUser,
) {
const record = await prisma.payment.findUnique({
From c47ffb5435a9c6f0a5d8672b2211adb426eed7b2 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 09:17:52 +0700
Subject: [PATCH 06/54] chore: migration
---
.../20250911021745_add_more_payment_metadata/migration.sql | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 prisma/migrations/20250911021745_add_more_payment_metadata/migration.sql
diff --git a/prisma/migrations/20250911021745_add_more_payment_metadata/migration.sql b/prisma/migrations/20250911021745_add_more_payment_metadata/migration.sql
new file mode 100644
index 0000000..27036c0
--- /dev/null
+++ b/prisma/migrations/20250911021745_add_more_payment_metadata/migration.sql
@@ -0,0 +1,4 @@
+-- AlterTable
+ALTER TABLE "Payment" ADD COLUMN "account" TEXT,
+ADD COLUMN "channel" TEXT,
+ADD COLUMN "reference" TEXT;
From d3c5b49649004f4a6a8da3464f6cb4f1a12e0064 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 09:32:38 +0700
Subject: [PATCH 07/54] chore: update prisma
---
package.json | 4 +-
pnpm-lock.yaml | 298 ++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 250 insertions(+), 52 deletions(-)
diff --git a/package.json b/package.json
index 13cd0e0..7fc3a6c 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
- "prisma": "^6.3.0",
+ "prisma": "^6.16.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
@@ -40,7 +40,7 @@
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
- "@prisma/client": "^6.3.0",
+ "@prisma/client": "^6.16.0",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8d3df75..8bbe67b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
specifier: ^5.0.2
version: 5.0.2
'@prisma/client':
- specifier: ^6.3.0
- version: 6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)
+ specifier: ^6.16.0
+ version: 6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)
'@scalar/express-api-reference':
specifier: ^0.4.182
version: 0.4.182
@@ -82,7 +82,7 @@ importers:
version: 6.10.0
prisma-extension-kysely:
specifier: ^3.0.0
- version: 3.0.0(@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2))
+ version: 3.0.0(@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2))
promise.any:
specifier: ^2.0.6
version: 2.0.6
@@ -133,8 +133,8 @@ importers:
specifier: ^3.4.2
version: 3.4.2
prisma:
- specifier: ^6.3.0
- version: 6.3.0(typescript@5.7.2)
+ specifier: ^6.16.0
+ version: 6.16.0(typescript@5.7.2)
prisma-kysely:
specifier: ^1.8.0
version: 1.8.0(encoding@0.1.13)
@@ -146,7 +146,7 @@ importers:
version: 5.7.2
vitest:
specifier: ^3.1.4
- version: 3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(yaml@2.6.1)
+ version: 3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(jiti@2.5.1)(yaml@2.6.1)
packages:
@@ -522,8 +522,8 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
- '@prisma/client@6.3.0':
- resolution: {integrity: sha512-BY3Fi28PUSk447Bpv22LhZp4HgNPo7NsEN+EteM1CLDnLjig5863jpW+3c3HHLFmml+nB/eJv1CjSriFZ8z7Cg==}
+ '@prisma/client@6.16.0':
+ resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
@@ -534,26 +534,29 @@ packages:
typescript:
optional: true
+ '@prisma/config@6.16.0':
+ resolution: {integrity: sha512-Q9TgfnllVehvQziY9lJwRJLGmziX0OimZUEQ/MhCUBoJMSScj2VivCjw/Of2vlO1FfyaHXxrvjZAr7ASl7DVcw==}
+
'@prisma/debug@5.3.1':
resolution: {integrity: sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==}
- '@prisma/debug@6.3.0':
- resolution: {integrity: sha512-m1lQv//0Rc5RG8TBpNUuLCxC35Ghi5XfpPmL83Gh04/GICHD2J5H2ndMlaljrUNaQDF9dOxIuFAYP1rE9wkXkg==}
+ '@prisma/debug@6.16.0':
+ resolution: {integrity: sha512-bxzro5vbVqAPkWyDs2A6GpQtRZunD8tyrLmSAchx9u0b+gWCDY6eV+oh5A0YtYT9245dIxQBswckayHuJG4u3w==}
- '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0':
- resolution: {integrity: sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==}
+ '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43':
+ resolution: {integrity: sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==}
'@prisma/engines@5.3.1':
resolution: {integrity: sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==}
- '@prisma/engines@6.3.0':
- resolution: {integrity: sha512-RXqYhlZb9sx/xkUfYIZuEPn7sT0WgTxNOuEYQ7AGw3IMpP9QGVEDVsluc/GcNkM8NTJszeqk8AplJzI9lm7Jxw==}
+ '@prisma/engines@6.16.0':
+ resolution: {integrity: sha512-RHJGCH/zi017W4CWYWqg0Sv1pquGGFVo8T3auJ9sodDNaiRzbeNldydjaQzszVS8nscdtcvLuJzy7e65C3puqQ==}
'@prisma/fetch-engine@5.3.1':
resolution: {integrity: sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==}
- '@prisma/fetch-engine@6.3.0':
- resolution: {integrity: sha512-GBy0iT4f1mH31ePzfcpVSUa7JLRTeq4914FG2vR3LqDwRweSm4ja1o5flGDz+eVIa/BNYfkBvRRxv4D6ve6Eew==}
+ '@prisma/fetch-engine@6.16.0':
+ resolution: {integrity: sha512-Mx5rml0XRIDizhB9eZxSP8c0nMoXYVITTiJJwxlWn9rNCel8mG8NAqIw+vJlN3gPR+kt3IBkP1SQVsplPPpYrA==}
'@prisma/generator-helper@5.3.1':
resolution: {integrity: sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==}
@@ -561,8 +564,8 @@ packages:
'@prisma/get-platform@5.3.1':
resolution: {integrity: sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==}
- '@prisma/get-platform@6.3.0':
- resolution: {integrity: sha512-V8zZ1d0xfyi6FjpNP4AcYuwSpGcdmu35OXWnTPm8IW594PYALzKXHwIa9+o0f+Lo9AecFWrwrwaoYe56UNfTtQ==}
+ '@prisma/get-platform@6.16.0':
+ resolution: {integrity: sha512-eaJOOvAoGslSUTjiQrtE9E0hoBdfL43j8SymOGD6LbdrKRNtIoiy6qiBaEr2fNYD+R/Qns7QOwPhl7SVHJayKA==}
'@prisma/internals@5.3.1':
resolution: {integrity: sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==}
@@ -685,6 +688,9 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -1084,6 +1090,14 @@ packages:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
+ c12@3.1.0:
+ resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
+ peerDependencies:
+ magicast: ^0.3.5
+ peerDependenciesMeta:
+ magicast:
+ optional: true
+
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
@@ -1133,6 +1147,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
@@ -1140,6 +1158,9 @@ packages:
resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==}
engines: {node: '>=8'}
+ citty@0.1.6:
+ resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
+
cjs-module-lexer@1.4.1:
resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==}
@@ -1199,6 +1220,13 @@ packages:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
+ confbox@0.2.2:
+ resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
+
+ consola@3.4.2:
+ resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
+ engines: {node: ^14.18.0 || >=16.10.0}
+
console-log-level@1.4.1:
resolution: {integrity: sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==}
@@ -1319,6 +1347,10 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
+ deepmerge-ts@7.1.5:
+ resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
+ engines: {node: '>=16.0.0'}
+
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -1331,6 +1363,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
del@6.1.1:
resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
engines: {node: '>=10'}
@@ -1339,6 +1374,9 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -1384,6 +1422,10 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
+ dotenv@16.6.1:
+ resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+ engines: {node: '>=12'}
+
dunder-proto@1.0.0:
resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==}
engines: {node: '>= 0.4'}
@@ -1400,6 +1442,9 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ effect@3.16.12:
+ resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
+
elastic-apm-node@3.52.2:
resolution: {integrity: sha512-NVFthDcoBOpTwtppF7b+BIeIu4Xon3RBNpddIaJv+DtjL6Q61x4j7ClYdiXjv3XKgyp7yUlOnLjU6PY/EYXwLQ==}
engines: {node: '>=8.6.0'}
@@ -1410,6 +1455,10 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ empathic@2.0.0:
+ resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
+ engines: {node: '>=14'}
+
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
@@ -1528,6 +1577,13 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+
+ fast-check@3.23.2:
+ resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
+ engines: {node: '>=8.0.0'}
+
fast-csv@4.3.6:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
@@ -1685,6 +1741,10 @@ packages:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
+ giget@2.0.0:
+ resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
+ hasBin: true
+
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@@ -2038,6 +2098,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+ jiti@2.5.1:
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
+ hasBin: true
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2352,6 +2416,9 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+ node-fetch-native@1.6.7:
+ resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+
node-fetch@2.6.12:
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
engines: {node: 4.x || >=6.0.0}
@@ -2403,6 +2470,11 @@ packages:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
+ nypm@0.6.1:
+ resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==}
+ engines: {node: ^14.16.0 || >=16.10.0}
+ hasBin: true
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2432,6 +2504,9 @@ packages:
obliterator@2.0.4:
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
on-finished@2.3.0:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'}
@@ -2553,6 +2628,9 @@ packages:
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2575,6 +2653,9 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
+ pkg-types@2.3.0:
+ resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
@@ -2602,8 +2683,8 @@ packages:
resolution: {integrity: sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==}
hasBin: true
- prisma@6.3.0:
- resolution: {integrity: sha512-y+Zh3Qg+xGCWyyrNUUNaFW/OltaV/yXYuTa0WRgYkz5LGyifmAsgpv94I47+qGRocZrMGcbF2A/78/oO2zgifA==}
+ prisma@6.16.0:
+ resolution: {integrity: sha512-TTh+H1Kw8N68KN9cDzdAyMroqMOvdCO/Z+kS2wKEVYR1nuR21qH5Q/Db/bZHsAgw7l/TPHtM/veG5VABcdwPDw==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
@@ -2650,6 +2731,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ pure-rand@6.1.0:
+ resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
+
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -2672,6 +2756,9 @@ packages:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
+ rc9@2.1.2:
+ resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
+
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@@ -2698,6 +2785,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -3072,6 +3163,9 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+ tinyexec@1.0.1:
+ resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
+
tinyglobby@0.2.13:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
@@ -3885,11 +3979,20 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
- '@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)':
+ '@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)':
optionalDependencies:
- prisma: 6.3.0(typescript@5.7.2)
+ prisma: 6.16.0(typescript@5.7.2)
typescript: 5.7.2
+ '@prisma/config@6.16.0':
+ dependencies:
+ c12: 3.1.0
+ deepmerge-ts: 7.1.5
+ effect: 3.16.12
+ empathic: 2.0.0
+ transitivePeerDependencies:
+ - magicast
+
'@prisma/debug@5.3.1':
dependencies:
'@types/debug': 4.1.8
@@ -3898,18 +4001,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@prisma/debug@6.3.0': {}
+ '@prisma/debug@6.16.0': {}
- '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {}
+ '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': {}
'@prisma/engines@5.3.1': {}
- '@prisma/engines@6.3.0':
+ '@prisma/engines@6.16.0':
dependencies:
- '@prisma/debug': 6.3.0
- '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0
- '@prisma/fetch-engine': 6.3.0
- '@prisma/get-platform': 6.3.0
+ '@prisma/debug': 6.16.0
+ '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43
+ '@prisma/fetch-engine': 6.16.0
+ '@prisma/get-platform': 6.16.0
'@prisma/fetch-engine@5.3.1(encoding@0.1.13)':
dependencies:
@@ -3934,11 +4037,11 @@ snapshots:
- encoding
- supports-color
- '@prisma/fetch-engine@6.3.0':
+ '@prisma/fetch-engine@6.16.0':
dependencies:
- '@prisma/debug': 6.3.0
- '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0
- '@prisma/get-platform': 6.3.0
+ '@prisma/debug': 6.16.0
+ '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43
+ '@prisma/get-platform': 6.16.0
'@prisma/generator-helper@5.3.1':
dependencies:
@@ -3964,9 +4067,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@prisma/get-platform@6.3.0':
+ '@prisma/get-platform@6.16.0':
dependencies:
- '@prisma/debug': 6.3.0
+ '@prisma/debug': 6.16.0
'@prisma/internals@5.3.1(encoding@0.1.13)':
dependencies:
@@ -4094,6 +4197,8 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
+ '@standard-schema/spec@1.0.0': {}
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -4273,13 +4378,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
- '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@20.17.10)(yaml@2.6.1))':
+ '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1))':
dependencies:
'@vitest/spy': 3.1.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
- vite: 6.3.5(@types/node@20.17.10)(yaml@2.6.1)
+ vite: 6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1)
'@vitest/pretty-format@3.1.4':
dependencies:
@@ -4309,7 +4414,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.13
tinyrainbow: 2.0.0
- vitest: 3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(yaml@2.6.1)
+ vitest: 3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(jiti@2.5.1)(yaml@2.6.1)
'@vitest/utils@3.1.4':
dependencies:
@@ -4583,6 +4688,21 @@ snapshots:
bytes@3.1.2: {}
+ c12@3.1.0:
+ dependencies:
+ chokidar: 4.0.3
+ confbox: 0.2.2
+ defu: 6.1.4
+ dotenv: 16.6.1
+ exsolve: 1.0.7
+ giget: 2.0.0
+ jiti: 2.5.1
+ ohash: 2.0.11
+ pathe: 2.0.3
+ perfect-debounce: 1.0.0
+ pkg-types: 2.3.0
+ rc9: 2.1.2
+
cac@6.7.14: {}
call-bind-apply-helpers@1.0.1:
@@ -4662,10 +4782,18 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
chownr@1.1.4: {}
ci-info@3.8.0: {}
+ citty@0.1.6:
+ dependencies:
+ consola: 3.4.2
+
cjs-module-lexer@1.4.1:
optional: true
@@ -4741,6 +4869,10 @@ snapshots:
readable-stream: 2.3.8
typedarray: 0.0.6
+ confbox@0.2.2: {}
+
+ consola@3.4.2: {}
+
console-log-level@1.4.1:
optional: true
@@ -4840,6 +4972,8 @@ snapshots:
deep-extend@0.6.0: {}
+ deepmerge-ts@7.1.5: {}
+
deepmerge@4.3.1: {}
define-data-property@1.1.4:
@@ -4854,6 +4988,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ defu@6.1.4: {}
+
del@6.1.1:
dependencies:
globby: 11.1.0
@@ -4867,6 +5003,8 @@ snapshots:
depd@2.0.0: {}
+ destr@2.0.5: {}
+
destroy@1.2.0: {}
detect-libc@2.0.4: {}
@@ -4906,6 +5044,8 @@ snapshots:
dotenv@16.4.7: {}
+ dotenv@16.6.1: {}
+
dunder-proto@1.0.0:
dependencies:
call-bind-apply-helpers: 1.0.1
@@ -4924,6 +5064,11 @@ snapshots:
ee-first@1.1.1: {}
+ effect@3.16.12:
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ fast-check: 3.23.2
+
elastic-apm-node@3.52.2:
dependencies:
'@elastic/ecs-pino-format': 1.5.0
@@ -4972,6 +5117,8 @@ snapshots:
emoji-regex@9.2.2: {}
+ empathic@2.0.0: {}
+
enabled@2.0.0: {}
encodeurl@1.0.2: {}
@@ -5205,6 +5352,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ exsolve@1.0.7: {}
+
+ fast-check@3.23.2:
+ dependencies:
+ pure-rand: 6.1.0
+
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
@@ -5382,6 +5535,15 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.2.6
+ giget@2.0.0:
+ dependencies:
+ citty: 0.1.6
+ consola: 3.4.2
+ defu: 6.1.4
+ node-fetch-native: 1.6.7
+ nypm: 0.6.1
+ pathe: 2.0.3
+
github-from-package@0.0.0: {}
glob-parent@5.1.2:
@@ -5747,6 +5909,8 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
+ jiti@2.5.1: {}
+
js-tokens@4.0.0: {}
jsbarcode@3.11.6: {}
@@ -6032,6 +6196,8 @@ snapshots:
node-addon-api@7.1.1: {}
+ node-fetch-native@1.6.7: {}
+
node-fetch@2.6.12(encoding@0.1.13):
dependencies:
whatwg-url: 5.0.0
@@ -6085,6 +6251,14 @@ snapshots:
dependencies:
path-key: 3.1.1
+ nypm@0.6.1:
+ dependencies:
+ citty: 0.1.6
+ consola: 3.4.2
+ pathe: 2.0.3
+ pkg-types: 2.3.0
+ tinyexec: 1.0.1
+
object-assign@4.1.1: {}
object-filter-sequence@1.0.0:
@@ -6115,6 +6289,8 @@ snapshots:
obliterator@2.0.4: {}
+ ohash@2.0.11: {}
+
on-finished@2.3.0:
dependencies:
ee-first: 1.1.1
@@ -6224,6 +6400,8 @@ snapshots:
peberminta@0.9.0: {}
+ perfect-debounce@1.0.0: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -6248,6 +6426,12 @@ snapshots:
dependencies:
find-up: 4.1.0
+ pkg-types@2.3.0:
+ dependencies:
+ confbox: 0.2.2
+ exsolve: 1.0.7
+ pathe: 2.0.3
+
possible-typed-array-names@1.0.0: {}
postcss@8.5.3:
@@ -6273,9 +6457,9 @@ snapshots:
prettier@3.4.2: {}
- prisma-extension-kysely@3.0.0(@prisma/client@6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)):
+ prisma-extension-kysely@3.0.0(@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)):
dependencies:
- '@prisma/client': 6.3.0(prisma@6.3.0(typescript@5.7.2))(typescript@5.7.2)
+ '@prisma/client': 6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)
prisma-kysely@1.8.0(encoding@0.1.13):
dependencies:
@@ -6288,12 +6472,14 @@ snapshots:
- encoding
- supports-color
- prisma@6.3.0(typescript@5.7.2):
+ prisma@6.16.0(typescript@5.7.2):
dependencies:
- '@prisma/engines': 6.3.0
+ '@prisma/config': 6.16.0
+ '@prisma/engines': 6.16.0
optionalDependencies:
- fsevents: 2.3.3
typescript: 5.7.2
+ transitivePeerDependencies:
+ - magicast
process-nextick-args@2.0.1: {}
@@ -6339,6 +6525,8 @@ snapshots:
punycode@2.3.1:
optional: true
+ pure-rand@6.1.0: {}
+
qs@6.13.0:
dependencies:
side-channel: 1.1.0
@@ -6364,6 +6552,11 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
+ rc9@2.1.2:
+ dependencies:
+ defu: 6.1.4
+ destr: 2.0.5
+
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
@@ -6408,6 +6601,8 @@ snapshots:
dependencies:
picomatch: 2.3.1
+ readdirp@4.1.2: {}
+
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.8:
@@ -6853,6 +7048,8 @@ snapshots:
tinyexec@0.3.2: {}
+ tinyexec@1.0.1: {}
+
tinyglobby@0.2.13:
dependencies:
fdir: 6.4.4(picomatch@4.0.2)
@@ -7058,13 +7255,13 @@ snapshots:
vary@1.1.2: {}
- vite-node@3.1.4(@types/node@20.17.10)(yaml@2.6.1):
+ vite-node@3.1.4(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1):
dependencies:
cac: 6.7.14
debug: 4.4.0(supports-color@5.5.0)
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 6.3.5(@types/node@20.17.10)(yaml@2.6.1)
+ vite: 6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -7079,7 +7276,7 @@ snapshots:
- tsx
- yaml
- vite@6.3.5(@types/node@20.17.10)(yaml@2.6.1):
+ vite@6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@@ -7090,12 +7287,13 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.10
fsevents: 2.3.3
+ jiti: 2.5.1
yaml: 2.6.1
- vitest@3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(yaml@2.6.1):
+ vitest@3.1.4(@types/node@20.17.10)(@vitest/ui@3.1.4)(jiti@2.5.1)(yaml@2.6.1):
dependencies:
'@vitest/expect': 3.1.4
- '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@20.17.10)(yaml@2.6.1))
+ '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1))
'@vitest/pretty-format': 3.1.4
'@vitest/runner': 3.1.4
'@vitest/snapshot': 3.1.4
@@ -7112,8 +7310,8 @@ snapshots:
tinyglobby: 0.2.13
tinypool: 1.0.2
tinyrainbow: 2.0.0
- vite: 6.3.5(@types/node@20.17.10)(yaml@2.6.1)
- vite-node: 3.1.4(@types/node@20.17.10)(yaml@2.6.1)
+ vite: 6.3.5(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1)
+ vite-node: 3.1.4(@types/node@20.17.10)(jiti@2.5.1)(yaml@2.6.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.17.10
From 5674a18cc3e46edec63a322cf3282384fc6177f0 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 11:48:17 +0700
Subject: [PATCH 08/54] fix: price calc match with flow account
---
src/controllers/05-quotation-controller.ts | 43 +++++++++++-----------
1 file changed, 21 insertions(+), 22 deletions(-)
diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts
index 29face5..d283dbb 100644
--- a/src/controllers/05-quotation-controller.ts
+++ b/src/controllers/05-quotation-controller.ts
@@ -527,14 +527,13 @@ export class QuotationController extends Controller {
const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = body.agentPrice ? p.agentPrice : p.price;
- const finalPriceWithVat = precisionRound(
+ const finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
-
- const price = finalPriceWithVat;
- const pricePerUnit = price / (1 + VAT_DEFAULT);
+ const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat)
- ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
+ ? pricePerUnit * (1 + VAT_DEFAULT) * v.amount -
+ ((v.discount || 0) / (1 + VAT_DEFAULT)) * VAT_DEFAULT
: 0;
return {
@@ -557,13 +556,13 @@ export class QuotationController extends Controller {
const price = list.reduce(
(a, c) => {
- a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
+ 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.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
- a.vatExcluded =
- c.vat === 0
- ? precisionRound(a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)))
- : a.vatExcluded;
+ a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@@ -815,14 +814,13 @@ export class QuotationController extends Controller {
const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded;
const originalPrice = record.agentPrice ? p.agentPrice : p.price;
- const finalPriceWithVat = precisionRound(
+ const finalPrice = precisionRound(
originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
-
- const price = finalPriceWithVat;
- const pricePerUnit = price / (1 + VAT_DEFAULT);
+ const pricePerUnit = finalPrice / (1 + VAT_DEFAULT);
const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat)
- ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT
+ ? pricePerUnit * (1 + VAT_DEFAULT) * v.amount -
+ ((v.discount || 0) / (1 + VAT_DEFAULT)) * VAT_DEFAULT
: 0;
return {
@@ -845,15 +843,13 @@ export class QuotationController extends Controller {
const price = list?.reduce(
(a, c) => {
- a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
+ 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.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
- a.vatExcluded =
- c.vat === 0
- ? precisionRound(
- a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
- )
- : a.vatExcluded;
+ a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@@ -869,6 +865,9 @@ export class QuotationController extends Controller {
finalPrice: 0,
},
);
+
+ console.log(price);
+
const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => {
return (
From 86db927efe0fca149c18a0bec1173ae2e739d410 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 13:57:23 +0700
Subject: [PATCH 09/54] fix: price calc
---
src/controllers/05-quotation-controller.ts | 13 +++--
src/controllers/09-debit-note-controller.ts | 60 ++++++++++++---------
2 files changed, 40 insertions(+), 33 deletions(-)
diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts
index d283dbb..942e4d1 100644
--- a/src/controllers/05-quotation-controller.ts
+++ b/src/controllers/05-quotation-controller.ts
@@ -532,10 +532,10 @@ export class QuotationController extends Controller {
);
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
+ ? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
+ (1 + VAT_DEFAULT)) *
+ VAT_DEFAULT
: 0;
-
return {
order: i + 1,
productId: v.productId,
@@ -819,8 +819,9 @@ export class QuotationController extends Controller {
);
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
+ ? ((pricePerUnit * (1 + VAT_DEFAULT) * v.amount - (v.discount || 0)) /
+ (1 + VAT_DEFAULT)) *
+ VAT_DEFAULT
: 0;
return {
@@ -866,8 +867,6 @@ export class QuotationController extends Controller {
},
);
- console.log(price);
-
const changed = list?.some((lhs) => {
const found = record.productServiceList.find((rhs) => {
return (
diff --git a/src/controllers/09-debit-note-controller.ts b/src/controllers/09-debit-note-controller.ts
index 5fcdee9..f87bbe9 100644
--- a/src/controllers/09-debit-note-controller.ts
+++ b/src/controllers/09-debit-note-controller.ts
@@ -430,12 +430,18 @@ export class DebitNoteController extends Controller {
const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
- const price = body.agentPrice ? p.agentPrice : p.price;
- const pricePerUnit = p.vatIncluded ? 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)
+
+ 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
: 0;
return {
@@ -458,15 +464,13 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
- a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
+ 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.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
- a.vatExcluded =
- c.vat === 0
- ? precisionRound(
- a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
- )
- : a.vatExcluded;
+ a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
@@ -673,12 +677,18 @@ export class DebitNoteController extends Controller {
}
const list = body.productServiceList.map((v, i) => {
const p = product.find((p) => p.id === v.productId)!;
- const price = body.agentPrice ? p.agentPrice : p.price;
- const pricePerUnit = p.vatIncluded ? 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)
+
+ 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
: 0;
return {
@@ -701,15 +711,13 @@ export class DebitNoteController extends Controller {
const price = list.reduce(
(a, c) => {
- a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount);
+ 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.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + c.vat);
- a.vatExcluded =
- c.vat === 0
- ? precisionRound(
- a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT,
- )
- : a.vatExcluded;
+ a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + price) : a.vatExcluded;
a.finalPrice = precisionRound(
Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0),
);
From eda0edbd292116f81246b632ffd93e59ee126c0d Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 11 Sep 2025 17:23:21 +0700
Subject: [PATCH 10/54] feat: send remark to flowaccount
---
src/services/flowaccount.ts | 41 +++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 730482d..80c0ca0 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -1,6 +1,8 @@
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";
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");
@@ -232,6 +234,29 @@ 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,
@@ -326,6 +351,22 @@ const flowAccount = {
? 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: product.map((v) => ({
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
From 8f2810ea294641b01f1dcf85d683cc7bb5adaa77 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 12 Sep 2025 11:56:40 +0700
Subject: [PATCH 11/54] feat: add flowAccountProductId field
---
prisma/schema.prisma | 3 +++
1 file changed, 3 insertions(+)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 78609d6..2f25bb2 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1243,6 +1243,9 @@ model Product {
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
productGroupId String
+ flowAccountProductIdSellPrice String?
+ flowAccountProductIdAgentPrice String?
+
workProduct WorkProduct[]
quotationProductServiceList QuotationProductServiceList[]
taskProduct TaskProduct[]
From c774e9f44c5b0a243d7188059e9e5d3a369cf0f0 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 12 Sep 2025 11:56:58 +0700
Subject: [PATCH 12/54] chore: migration
---
.../migration.sql | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 prisma/migrations/20250911092303_add_flow_account_product_id_sell_price_and_flow_account_product_id_agent_price_field_in_product_table/migration.sql
diff --git a/prisma/migrations/20250911092303_add_flow_account_product_id_sell_price_and_flow_account_product_id_agent_price_field_in_product_table/migration.sql b/prisma/migrations/20250911092303_add_flow_account_product_id_sell_price_and_flow_account_product_id_agent_price_field_in_product_table/migration.sql
new file mode 100644
index 0000000..0ef2494
--- /dev/null
+++ b/prisma/migrations/20250911092303_add_flow_account_product_id_sell_price_and_flow_account_product_id_agent_price_field_in_product_table/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "public"."Product" ADD COLUMN "flowAccountProductIdAgentPrice" TEXT,
+ADD COLUMN "flowAccountProductIdSellPrice" TEXT;
From 250bbca226f69b269808e425d92a02cf88fad619 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 12 Sep 2025 15:19:51 +0700
Subject: [PATCH 13/54] feat: create product for flowAccount service
---
src/services/flowaccount.ts | 184 ++++++++++++++++++++++++++++++++++++
1 file changed, 184 insertions(+)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 80c0ca0..df20be0 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -3,6 +3,7 @@ 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";
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");
@@ -388,6 +389,189 @@ 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(body: JsonObject) {
+ const { token } = await flowAccountAPI.auth();
+
+ const commonBody = {
+ productStructureType: null,
+ type: "3",
+ name: body.name,
+ sellDescription: body.detail,
+ sellVatType: 3,
+ unitName: "Unit",
+ categoryName: "Car",
+ }; // helper function สำหรับสร้าง product
+
+ const createProduct = async (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,
+ sellPrice: price,
+ sellVatType: vatIncluded ? 1 : 3,
+ }),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Request failed with status ${res.status}`);
+ } // ป้องกัน response ที่ไม่ใช่ JSON หรือว่าง
+
+ 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;
+ }
+ };
+
+ const [sellId, agentId] = await Promise.all([
+ createProduct(body.price, /true/.test(`${body.vatIncluded}`)),
+ createProduct(body.agentPrice, /true/.test(`${body.agentPriceVatIncluded}`)),
+ ]);
+
+ return {
+ ok: !!(agentId && sellId),
+ status: agentId && sellId ? 200 : 500,
+ data: {
+ productIdAgentPrice: agentId,
+ productIdSellPrice: sellId,
+ },
+ };
+ },
+
+ // flowAccount PUT edit Product
+ async editProducts(sellPriceId: String, agentPriceId: String, body: JsonObject) {
+ console.log("body: ", body);
+ const { token } = await flowAccountAPI.auth();
+
+ const commonBody = {
+ productStructureType: null,
+ type: "3",
+ name: body.name,
+ sellDescription: body.detail,
+ sellVatType: 3,
+ unitName: "Unit",
+ categoryName: "Car",
+ }; // helper function สำหรับสร้าง product
+
+ const editProduct = async (id: 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,
+ sellPrice: price,
+ sellVatType: vatIncluded ? 1 : 3,
+ }),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Request failed with status ${res.status}`);
+ } // ป้องกัน response ที่ไม่ใช่ JSON หรือว่าง
+
+ 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;
+ }
+ };
+
+ const [agentId, sellId] = await Promise.all([
+ editProduct(sellPriceId, body.price, /true/.test(`${body.vatIncluded}`)),
+ editProduct(agentPriceId, body.agentPrice, /true/.test(`${body.agentPriceVatIncluded}`)),
+ ]);
+
+ return {
+ ok: !!(agentId && sellId),
+ status: agentId && sellId ? 200 : 500,
+ data: {
+ productIdAgentPrice: agentId,
+ productIdSellPrice: sellId,
+ },
+ };
+ },
+
+ // 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;
From 61825309d1224a42a9cf14c08b57278d64bdd3b1 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 12 Sep 2025 15:20:20 +0700
Subject: [PATCH 14/54] feat: sync data from data to flowAccount
---
src/controllers/04-product-controller.ts | 31 ++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts
index b50462c..c1c40d8 100644
--- a/src/controllers/04-product-controller.ts
+++ b/src/controllers/04-product-controller.ts
@@ -30,6 +30,7 @@ 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";
const MANAGE_ROLES = [
"system",
@@ -299,6 +300,9 @@ export class ProductController extends Controller {
},
update: { value: { increment: 1 } },
});
+
+ const listId = await flowAccount.createProducts(body);
+
return await prisma.product.create({
include: {
createdBy: true,
@@ -306,6 +310,8 @@ export class ProductController extends Controller {
},
data: {
...body,
+ flowAccountProductIdAgentPrice: `${listId.data.productIdAgentPrice}`,
+ flowAccountProductIdSellPrice: `${listId.data.productIdSellPrice}`,
document: body.document
? {
createMany: { data: body.document.map((v) => ({ name: v })) },
@@ -379,6 +385,19 @@ export class ProductController extends Controller {
await permissionCheck(req.user, productGroup.registeredBranch);
}
+ if (
+ product.flowAccountProductIdSellPrice !== null &&
+ product.flowAccountProductIdAgentPrice !== null
+ ) {
+ await flowAccount.editProducts(
+ product.flowAccountProductIdSellPrice,
+ product.flowAccountProductIdAgentPrice,
+ body,
+ );
+ } else {
+ throw notFoundError("FlowAccountProductId");
+ }
+
const record = await prisma.product.update({
include: {
productGroup: true,
@@ -441,6 +460,18 @@ 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({
From d51531cd4db8e34c97898c06b54847d1c2730cb2 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 12 Sep 2025 15:29:52 +0700
Subject: [PATCH 15/54] chore: clean
---
src/services/flowaccount.ts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index df20be0..49060e0 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -487,15 +487,14 @@ const flowAccount = {
ok: !!(agentId && sellId),
status: agentId && sellId ? 200 : 500,
data: {
- productIdAgentPrice: agentId,
productIdSellPrice: sellId,
+ productIdAgentPrice: agentId,
},
};
},
// flowAccount PUT edit Product
async editProducts(sellPriceId: String, agentPriceId: String, body: JsonObject) {
- console.log("body: ", body);
const { token } = await flowAccountAPI.auth();
const commonBody = {
@@ -541,7 +540,7 @@ const flowAccount = {
}
};
- const [agentId, sellId] = await Promise.all([
+ const [sellId, agentId] = await Promise.all([
editProduct(sellPriceId, body.price, /true/.test(`${body.vatIncluded}`)),
editProduct(agentPriceId, body.agentPrice, /true/.test(`${body.agentPriceVatIncluded}`)),
]);
@@ -550,8 +549,8 @@ const flowAccount = {
ok: !!(agentId && sellId),
status: agentId && sellId ? 200 : 500,
data: {
- productIdAgentPrice: agentId,
productIdSellPrice: sellId,
+ productIdAgentPrice: agentId,
},
};
},
From 1486ce79abf710101ea8a8783856c50da42e5381 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Mon, 15 Sep 2025 10:50:40 +0700
Subject: [PATCH 16/54] fix: calculate credit note price
---
src/controllers/08-credit-note-controller.ts | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/controllers/08-credit-note-controller.ts b/src/controllers/08-credit-note-controller.ts
index 48ee2ab..1b3c75d 100644
--- a/src/controllers/08-credit-note-controller.ts
+++ b/src/controllers/08-credit-note-controller.ts
@@ -13,6 +13,7 @@ import {
Security,
Tags,
} from "tsoa";
+import config from "../config.json";
import prisma from "../db";
@@ -53,6 +54,7 @@ function globalAllow(user: RequestWithUser["user"]) {
const listAllowed = ["system", "head_of_admin", "admin", "executive", "accountant"];
return user.roles?.some((v) => listAllowed.includes(v)) || false;
}
+const VAT_DEFAULT = config.vat;
const permissionCond = createPermCondition(globalAllow);
const permissionCheck = createPermCheck(globalAllow);
@@ -346,9 +348,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
- c.productService.pricePerUnit -
- c.productService.discount / c.productService.amount +
- c.productService.vat / c.productService.amount;
+ c.productService.pricePerUnit *
+ (1 + (c.productService.product.serviceChargeCalcVat ? VAT_DEFAULT : 0)) -
+ c.productService.discount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
@@ -424,7 +426,6 @@ export class CreditNoteController extends Controller {
let textData = "";
let dataCustomerId: string[] = [];
- let textWorkList: string[] = [];
let dataUserId: string[] = [];
if (res) {
@@ -541,9 +542,9 @@ export class CreditNoteController extends Controller {
).length;
const price =
- c.productService.pricePerUnit -
- c.productService.discount / c.productService.amount +
- c.productService.vat / c.productService.amount;
+ c.productService.pricePerUnit *
+ (1 + (c.productService.product.serviceChargeCalcVat ? VAT_DEFAULT : 0)) -
+ c.productService.discount;
if (serviceChargeStepCount && successCount) {
return a + price - c.productService.product.serviceCharge * successCount;
From f2def1b9623cbb7987f449aa814883895b0b746b Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Mon, 15 Sep 2025 16:43:01 +0700
Subject: [PATCH 17/54] fix: wrong calc vat condition
---
src/controllers/08-credit-note-controller.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/controllers/08-credit-note-controller.ts b/src/controllers/08-credit-note-controller.ts
index 1b3c75d..7cd029e 100644
--- a/src/controllers/08-credit-note-controller.ts
+++ b/src/controllers/08-credit-note-controller.ts
@@ -348,8 +348,7 @@ export class CreditNoteController extends Controller {
).length;
const price =
- c.productService.pricePerUnit *
- (1 + (c.productService.product.serviceChargeCalcVat ? VAT_DEFAULT : 0)) -
+ c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
if (serviceChargeStepCount && successCount) {
@@ -542,8 +541,7 @@ export class CreditNoteController extends Controller {
).length;
const price =
- c.productService.pricePerUnit *
- (1 + (c.productService.product.serviceChargeCalcVat ? VAT_DEFAULT : 0)) -
+ c.productService.pricePerUnit * (1 + (c.productService.vat > 0 ? VAT_DEFAULT : 0)) -
c.productService.discount;
if (serviceChargeStepCount && successCount) {
From 892d76583fc894d0547445b116276dc57b78f32c Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Tue, 16 Sep 2025 09:48:59 +0700
Subject: [PATCH 18/54] chore: flowaccount enable inline vat
---
src/services/flowaccount.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 49060e0..0bb5512 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -327,6 +327,7 @@ const flowAccount = {
dueDate: quotation.dueDate,
salesName: [quotation.createdBy?.firstName, quotation.createdBy?.lastName].join(" "),
+ useInlineVat: true,
isVatInclusive: true,
isVat: true,
From 6776188f7beb4b9e7872e233b52ed351eaba3a41 Mon Sep 17 00:00:00 2001
From: HAM
Date: Tue, 16 Sep 2025 09:50:23 +0700
Subject: [PATCH 19/54] fix: decimal number
---
src/services/flowaccount.ts | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 0bb5512..fde345d 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -372,7 +372,7 @@ const flowAccount = {
items: product.map((v) => ({
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
- pricePerUnit: v.pricePerUnit,
+ pricePerUnit: Math.round(v.pricePerUnit * 100) / 100,
quantity: v.amount,
discountAmount: v.discount,
total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
@@ -443,8 +443,7 @@ const flowAccount = {
sellDescription: body.detail,
sellVatType: 3,
unitName: "Unit",
- categoryName: "Car",
- }; // helper function สำหรับสร้าง product
+ };
const createProduct = async (price: any, vatIncluded: boolean) => {
try {
@@ -463,7 +462,7 @@ const flowAccount = {
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}`);
- } // ป้องกัน response ที่ไม่ใช่ JSON หรือว่าง
+ }
let json: any = null;
try {
@@ -506,7 +505,7 @@ const flowAccount = {
sellVatType: 3,
unitName: "Unit",
categoryName: "Car",
- }; // helper function สำหรับสร้าง product
+ };
const editProduct = async (id: String, price: any, vatIncluded: boolean) => {
try {
@@ -525,7 +524,7 @@ const flowAccount = {
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}`);
- } // ป้องกัน response ที่ไม่ใช่ JSON หรือว่าง
+ }
let json: any = null;
try {
From 4e71343af7cae29af0f4d9496ec004364e046cbd Mon Sep 17 00:00:00 2001
From: HAM
Date: Tue, 16 Sep 2025 09:53:03 +0700
Subject: [PATCH 20/54] refactor: use function for correct decimal number
---
src/services/flowaccount.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index fde345d..ec934ca 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -4,6 +4,7 @@ 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");
@@ -185,6 +186,8 @@ const flowAccountAPI = {
data.paymentDate = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`;
} */
+ console.log(JSON.stringify(data, null, 2));
+
const res = await fetch(
api + "/upgrade/receipts/inline" + (withPayment ? "/with-payment" : ""),
{
@@ -327,7 +330,6 @@ const flowAccount = {
dueDate: quotation.dueDate,
salesName: [quotation.createdBy?.firstName, quotation.createdBy?.lastName].join(" "),
- useInlineVat: true,
isVatInclusive: true,
isVat: true,
@@ -372,7 +374,7 @@ const flowAccount = {
items: product.map((v) => ({
type: ProductAndServiceType.ProductNonInv,
name: v.product.name,
- pricePerUnit: Math.round(v.pricePerUnit * 100) / 100,
+ pricePerUnit: precisionRound(v.pricePerUnit),
quantity: v.amount,
discountAmount: v.discount,
total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
From de33d036314a4e260fa8cd029060a5be8811a725 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Tue, 16 Sep 2025 10:08:01 +0700
Subject: [PATCH 21/54] fix: calculate price
---
src/services/flowaccount.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index ec934ca..31c2fd6 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -334,6 +334,7 @@ const flowAccount = {
isVat: true,
useReceiptDeduction: false,
+ useInlineVat: true,
discounPercentage: 0,
discountAmount: quotation.totalDiscount,
@@ -377,7 +378,9 @@ const flowAccount = {
pricePerUnit: precisionRound(v.pricePerUnit),
quantity: v.amount,
discountAmount: v.discount,
- total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat,
+ total:
+ precisionRound(v.pricePerUnit * (1 + (v.vat === 0 ? VAT_DEFAULT : 0))) * v.amount -
+ (v.discount ?? 0),
vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
})),
};
From 25a4b50f8eae8f5b2d24e7f6bbcdd28b00ad386a Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Tue, 16 Sep 2025 10:11:02 +0700
Subject: [PATCH 22/54] fix: wrong condition
---
src/services/flowaccount.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 31c2fd6..90eb5c0 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -379,7 +379,7 @@ const flowAccount = {
quantity: v.amount,
discountAmount: v.discount,
total:
- precisionRound(v.pricePerUnit * (1 + (v.vat === 0 ? VAT_DEFAULT : 0))) * v.amount -
+ precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_DEFAULT : 0))) * v.amount -
(v.discount ?? 0),
vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
})),
From 0772e4710a85bd7da732a4dd7d3f193ecc52f22d Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:44:13 +0700
Subject: [PATCH 23/54] fix: cannot update payment data after set payment
completed
---
src/controllers/05-payment-controller.ts | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts
index c61d4bc..7ad9584 100644
--- a/src/controllers/05-payment-controller.ts
+++ b/src/controllers/05-payment-controller.ts
@@ -152,7 +152,18 @@ export class QuotationPayment extends Controller {
if (!record) throw notFoundError("Payment");
- if (record.paymentStatus === "PaymentSuccess") return record;
+ 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,
+ },
+ });
+ }
return await prisma.$transaction(async (tx) => {
const current = new Date();
From 158a6ff1631f333e17914cf999c9a549c163e6d7 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:52:13 +0700
Subject: [PATCH 24/54] fix: timeout in some case
---
src/utils/thailand-area.ts | 274 +++++++++++++++++++------------------
1 file changed, 142 insertions(+), 132 deletions(-)
diff --git a/src/utils/thailand-area.ts b/src/utils/thailand-area.ts
index 1319e0b..7bcf360 100644
--- a/src/utils/thailand-area.ts
+++ b/src/utils/thailand-area.ts
@@ -62,85 +62,90 @@ 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();
- }),
- );
- });
+ 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,
+ },
+ );
console.log("[INFO]: Sync thailand province, district and subdistrict, OK.");
}
@@ -170,67 +175,72 @@ 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,
+ 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,
+ 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(),
- );
- });
+ 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(),
+ );
+ },
+ {
+ timeout: 15_000,
+ },
+ );
console.log("[INFO]: Sync employment office, OK.");
}
From 2c790de6066bd0bd15a1d7d81ce956313ec8f78f Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Wed, 17 Sep 2025 09:51:35 +0700
Subject: [PATCH 25/54] fix: customer status not change after used
---
src/controllers/05-quotation-controller.ts | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts
index 942e4d1..f85b125 100644
--- a/src/controllers/05-quotation-controller.ts
+++ b/src/controllers/05-quotation-controller.ts
@@ -900,6 +900,20 @@ 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: {
From 6598cd3bdfc4e67419ccd3f75103d5783c024ede Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Wed, 17 Sep 2025 10:42:35 +0700
Subject: [PATCH 26/54] fix: error field ambigous
---
package.json | 4 +-
pnpm-lock.yaml | 74 +++++++++++++-------------
src/controllers/00-stats-controller.ts | 20 ++++---
3 files changed, 53 insertions(+), 45 deletions(-)
diff --git a/package.json b/package.json
index 7fc3a6c..92e1d51 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
- "prisma": "^6.16.0",
+ "prisma": "^6.16.2",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
@@ -40,7 +40,7 @@
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
- "@prisma/client": "^6.16.0",
+ "@prisma/client": "^6.16.2",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8bbe67b..5c7f3d7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
specifier: ^5.0.2
version: 5.0.2
'@prisma/client':
- specifier: ^6.16.0
- version: 6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)
+ specifier: ^6.16.2
+ version: 6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2)
'@scalar/express-api-reference':
specifier: ^0.4.182
version: 0.4.182
@@ -82,7 +82,7 @@ importers:
version: 6.10.0
prisma-extension-kysely:
specifier: ^3.0.0
- version: 3.0.0(@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2))
+ version: 3.0.0(@prisma/client@6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2))
promise.any:
specifier: ^2.0.6
version: 2.0.6
@@ -133,8 +133,8 @@ importers:
specifier: ^3.4.2
version: 3.4.2
prisma:
- specifier: ^6.16.0
- version: 6.16.0(typescript@5.7.2)
+ specifier: ^6.16.2
+ version: 6.16.2(typescript@5.7.2)
prisma-kysely:
specifier: ^1.8.0
version: 1.8.0(encoding@0.1.13)
@@ -522,8 +522,8 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
- '@prisma/client@6.16.0':
- resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
+ '@prisma/client@6.16.2':
+ resolution: {integrity: sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
@@ -534,14 +534,14 @@ packages:
typescript:
optional: true
- '@prisma/config@6.16.0':
- resolution: {integrity: sha512-Q9TgfnllVehvQziY9lJwRJLGmziX0OimZUEQ/MhCUBoJMSScj2VivCjw/Of2vlO1FfyaHXxrvjZAr7ASl7DVcw==}
+ '@prisma/config@6.16.2':
+ resolution: {integrity: sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==}
'@prisma/debug@5.3.1':
resolution: {integrity: sha512-eYrxqslEKf+wpMFIIHgbcNYuZBXUdiJLA85Or3TwOhgPIN1ZoXT9CwJph3ynW8H1Xg0LkdYLwVmuULCwiMoU5A==}
- '@prisma/debug@6.16.0':
- resolution: {integrity: sha512-bxzro5vbVqAPkWyDs2A6GpQtRZunD8tyrLmSAchx9u0b+gWCDY6eV+oh5A0YtYT9245dIxQBswckayHuJG4u3w==}
+ '@prisma/debug@6.16.2':
+ resolution: {integrity: sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==}
'@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43':
resolution: {integrity: sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==}
@@ -549,14 +549,14 @@ packages:
'@prisma/engines@5.3.1':
resolution: {integrity: sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==}
- '@prisma/engines@6.16.0':
- resolution: {integrity: sha512-RHJGCH/zi017W4CWYWqg0Sv1pquGGFVo8T3auJ9sodDNaiRzbeNldydjaQzszVS8nscdtcvLuJzy7e65C3puqQ==}
+ '@prisma/engines@6.16.2':
+ resolution: {integrity: sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==}
'@prisma/fetch-engine@5.3.1':
resolution: {integrity: sha512-w1yk1YiK8N82Pobdq58b85l6e8akyrkxuzwV9DoiUTRf3gpsuhJJesHc4Yi0WzUC9/3znizl1UfCsI6dhkj3Vw==}
- '@prisma/fetch-engine@6.16.0':
- resolution: {integrity: sha512-Mx5rml0XRIDizhB9eZxSP8c0nMoXYVITTiJJwxlWn9rNCel8mG8NAqIw+vJlN3gPR+kt3IBkP1SQVsplPPpYrA==}
+ '@prisma/fetch-engine@6.16.2':
+ resolution: {integrity: sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==}
'@prisma/generator-helper@5.3.1':
resolution: {integrity: sha512-zrYS0iHLgPlOJjYnd5KvVMMvSS+ktOL39EwooS5EnyvfzwfzxlKCeOUgxTfiKYs0WUWqzEvyNAYtramYgSknsQ==}
@@ -564,8 +564,8 @@ packages:
'@prisma/get-platform@5.3.1':
resolution: {integrity: sha512-3IiZY2BUjKnAuZ0569zppZE6/rZbVAM09//c2nvPbbkGG9MqrirA8fbhhF7tfVmhyVfdmVCHnf/ujWPHJ8B46Q==}
- '@prisma/get-platform@6.16.0':
- resolution: {integrity: sha512-eaJOOvAoGslSUTjiQrtE9E0hoBdfL43j8SymOGD6LbdrKRNtIoiy6qiBaEr2fNYD+R/Qns7QOwPhl7SVHJayKA==}
+ '@prisma/get-platform@6.16.2':
+ resolution: {integrity: sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==}
'@prisma/internals@5.3.1':
resolution: {integrity: sha512-zkW73hPHHNrMD21PeYgCTBfMu71vzJf+WtfydtJbS0JVJKyLfOel0iWSQg7wjNeQfccKp+NdHJ/5rTJ4NEUzgA==}
@@ -2683,8 +2683,8 @@ packages:
resolution: {integrity: sha512-VpNpolZ8RXRgfU+j4R+fPZmX8EE95w3vJ2tt7+FwuiQc0leNTfLK5QLf3KbbPDes2rfjh3g20AjDxefQIo5GIA==}
hasBin: true
- prisma@6.16.0:
- resolution: {integrity: sha512-TTh+H1Kw8N68KN9cDzdAyMroqMOvdCO/Z+kS2wKEVYR1nuR21qH5Q/Db/bZHsAgw7l/TPHtM/veG5VABcdwPDw==}
+ prisma@6.16.2:
+ resolution: {integrity: sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
@@ -3979,12 +3979,12 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
- '@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)':
+ '@prisma/client@6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2)':
optionalDependencies:
- prisma: 6.16.0(typescript@5.7.2)
+ prisma: 6.16.2(typescript@5.7.2)
typescript: 5.7.2
- '@prisma/config@6.16.0':
+ '@prisma/config@6.16.2':
dependencies:
c12: 3.1.0
deepmerge-ts: 7.1.5
@@ -4001,18 +4001,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@prisma/debug@6.16.0': {}
+ '@prisma/debug@6.16.2': {}
'@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': {}
'@prisma/engines@5.3.1': {}
- '@prisma/engines@6.16.0':
+ '@prisma/engines@6.16.2':
dependencies:
- '@prisma/debug': 6.16.0
+ '@prisma/debug': 6.16.2
'@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43
- '@prisma/fetch-engine': 6.16.0
- '@prisma/get-platform': 6.16.0
+ '@prisma/fetch-engine': 6.16.2
+ '@prisma/get-platform': 6.16.2
'@prisma/fetch-engine@5.3.1(encoding@0.1.13)':
dependencies:
@@ -4037,11 +4037,11 @@ snapshots:
- encoding
- supports-color
- '@prisma/fetch-engine@6.16.0':
+ '@prisma/fetch-engine@6.16.2':
dependencies:
- '@prisma/debug': 6.16.0
+ '@prisma/debug': 6.16.2
'@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43
- '@prisma/get-platform': 6.16.0
+ '@prisma/get-platform': 6.16.2
'@prisma/generator-helper@5.3.1':
dependencies:
@@ -4067,9 +4067,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@prisma/get-platform@6.16.0':
+ '@prisma/get-platform@6.16.2':
dependencies:
- '@prisma/debug': 6.16.0
+ '@prisma/debug': 6.16.2
'@prisma/internals@5.3.1(encoding@0.1.13)':
dependencies:
@@ -6457,9 +6457,9 @@ snapshots:
prettier@3.4.2: {}
- prisma-extension-kysely@3.0.0(@prisma/client@6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)):
+ prisma-extension-kysely@3.0.0(@prisma/client@6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2)):
dependencies:
- '@prisma/client': 6.16.0(prisma@6.16.0(typescript@5.7.2))(typescript@5.7.2)
+ '@prisma/client': 6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2)
prisma-kysely@1.8.0(encoding@0.1.13):
dependencies:
@@ -6472,10 +6472,10 @@ snapshots:
- encoding
- supports-color
- prisma@6.16.0(typescript@5.7.2):
+ prisma@6.16.2(typescript@5.7.2):
dependencies:
- '@prisma/config': 6.16.0
- '@prisma/engines': 6.16.0
+ '@prisma/config': 6.16.2
+ '@prisma/engines': 6.16.2
optionalDependencies:
typescript: 5.7.2
transitivePeerDependencies:
diff --git a/src/controllers/00-stats-controller.ts b/src/controllers/00-stats-controller.ts
index 95484a4..47e803e 100644
--- a/src/controllers/00-stats-controller.ts
+++ b/src/controllers/00-stats-controller.ts
@@ -618,9 +618,22 @@ 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"),
@@ -629,12 +642,7 @@ export class StatsController extends Controller {
_sum: { amount: true },
where: {
createdAt: { gte: v, lte: date.endOf("month").toDate() },
- invoice: {
- quotation: {
- quotationStatus: { notIn: [QuotationStatus.Canceled] },
- registeredBranch: { OR: permissionCondCompany(req.user) },
- },
- },
+ invoiceId: { in: invoices.map((v) => v.id) },
},
by: "paymentStatus",
})
From beb7f4bcfe669120caa7396742f41f466098d16f Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Wed, 17 Sep 2025 11:34:54 +0700
Subject: [PATCH 27/54] fix: flowaccount contact name
---
src/services/flowaccount.ts | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 90eb5c0..33e09cf 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -300,10 +300,7 @@ const flowAccount = {
);
const payload = {
contactCode: customer.code,
- contactName:
- (customer.customer.customerType === CustomerType.PERS
- ? [customer.firstName, customer.lastName].join(" ").trim()
- : customer.registerName) || "-",
+ contactName: customer.contactName || "-",
contactAddress: [
customer.address,
!!customer.moo ? "หมู่ " + customer.moo : null,
@@ -312,7 +309,6 @@ 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(" "),
From 068ba2d293b5248eb2fb8af576e860140dad77a5 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Wed, 17 Sep 2025 11:53:15 +0700
Subject: [PATCH 28/54] fix: wrong tax
---
src/services/flowaccount.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 33e09cf..c7d0f4c 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -312,7 +312,7 @@ const flowAccount = {
]
.filter(Boolean)
.join(" "),
- contactTaxId: customer.citizenId || customer.code,
+ contactTaxId: customer.citizenId || customer.legalPersonNo,
contactBranch:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
From ab4ea4ba4b7fce3a354f422dd26b153233cbff04 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Wed, 17 Sep 2025 11:55:06 +0700
Subject: [PATCH 29/54] fix: type error
---
src/services/flowaccount.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index c7d0f4c..9450b02 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -312,7 +312,7 @@ const flowAccount = {
]
.filter(Boolean)
.join(" "),
- contactTaxId: customer.citizenId || customer.legalPersonNo,
+ contactTaxId: customer.citizenId || customer.legalPersonNo || "-",
contactBranch:
(customer.customer.customerType === CustomerType.PERS
? [customer.firstName, customer.lastName].join(" ").trim()
From 0d78ce4db3d7341f9828c125a8d6576ee8c7a6d8 Mon Sep 17 00:00:00 2001
From: HAM
Date: Wed, 17 Sep 2025 12:37:41 +0700
Subject: [PATCH 30/54] feat: filter businessType, province, district,
subDistrict for customer and customer-export endpoint
---
src/controllers/03-customer-controller.ts | 45 +++++++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 1854842..5a6ef36 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -170,6 +170,10 @@ export class CustomerController extends Controller {
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
+ @Query() businessType?: string,
+ @Query() province?: string,
+ @Query() district?: string,
+ @Query() subDistrict?: string,
) {
const where = {
OR: queryOrNot(query, [
@@ -192,6 +196,47 @@ export class CustomerController extends Controller {
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
},
},
+ branch: {
+ some: {
+ AND: [
+ businessType
+ ? {
+ OR: [
+ { businessType: { name: { contains: businessType, mode: "insensitive" } } },
+ { businessType: { nameEN: { contains: businessType, mode: "insensitive" } } },
+ ],
+ }
+ : {},
+
+ province
+ ? {
+ OR: [
+ { province: { name: { contains: province, mode: "insensitive" } } },
+ { province: { nameEN: { contains: province, mode: "insensitive" } } },
+ ],
+ }
+ : {},
+
+ district
+ ? {
+ OR: [
+ { district: { name: { contains: district, mode: "insensitive" } } },
+ { district: { nameEN: { contains: district, mode: "insensitive" } } },
+ ],
+ }
+ : {},
+
+ subDistrict
+ ? {
+ OR: [
+ { subDistrict: { name: { contains: subDistrict, mode: "insensitive" } } },
+ { subDistrict: { nameEN: { contains: subDistrict, mode: "insensitive" } } },
+ ],
+ }
+ : {},
+ ],
+ },
+ },
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerWhereInput;
From e5a3d948a5f3acf7037727505eb6b3a5b3aad181 Mon Sep 17 00:00:00 2001
From: HAM
Date: Wed, 17 Sep 2025 12:39:10 +0700
Subject: [PATCH 31/54] fix: missing query
---
src/controllers/03-customer-controller.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 5a6ef36..43dd376 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -642,6 +642,10 @@ export class CustomerExportController extends CustomerController {
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
+ @Query() businessType?: string,
+ @Query() province?: string,
+ @Query() district?: string,
+ @Query() subDistrict?: string,
) {
const ret = await this.list(
req,
@@ -655,6 +659,10 @@ export class CustomerExportController extends CustomerController {
activeBranchOnly,
startDate,
endDate,
+ businessType,
+ province,
+ district,
+ subDistrict,
);
this.setHeader("Content-Type", "text/csv");
From 7e7b8025c9884090e75e3fecd0109fdac7be1751 Mon Sep 17 00:00:00 2001
From: HAM
Date: Wed, 17 Sep 2025 12:58:14 +0700
Subject: [PATCH 32/54] refactor: change to id
---
src/controllers/03-customer-controller.ts | 20 ++++----------------
1 file changed, 4 insertions(+), 16 deletions(-)
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 43dd376..95e2df6 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -201,37 +201,25 @@ export class CustomerController extends Controller {
AND: [
businessType
? {
- OR: [
- { businessType: { name: { contains: businessType, mode: "insensitive" } } },
- { businessType: { nameEN: { contains: businessType, mode: "insensitive" } } },
- ],
+ OR: [{ businessType: { id: businessType } }],
}
: {},
province
? {
- OR: [
- { province: { name: { contains: province, mode: "insensitive" } } },
- { province: { nameEN: { contains: province, mode: "insensitive" } } },
- ],
+ OR: [{ province: { id: province } }],
}
: {},
district
? {
- OR: [
- { district: { name: { contains: district, mode: "insensitive" } } },
- { district: { nameEN: { contains: district, mode: "insensitive" } } },
- ],
+ OR: [{ district: { id: district } }],
}
: {},
subDistrict
? {
- OR: [
- { subDistrict: { name: { contains: subDistrict, mode: "insensitive" } } },
- { subDistrict: { nameEN: { contains: subDistrict, mode: "insensitive" } } },
- ],
+ OR: [{ subDistrict: { id: subDistrict } }],
}
: {},
],
From f50285161b02142019469e089f186b20cec6e511 Mon Sep 17 00:00:00 2001
From: HAM
Date: Thu, 18 Sep 2025 10:13:39 +0700
Subject: [PATCH 33/54] fix: variable name for filter
---
src/controllers/03-customer-controller.ts | 40 +++++++++++------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts
index 95e2df6..bd3bc4d 100644
--- a/src/controllers/03-customer-controller.ts
+++ b/src/controllers/03-customer-controller.ts
@@ -170,10 +170,10 @@ export class CustomerController extends Controller {
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
- @Query() businessType?: string,
- @Query() province?: string,
- @Query() district?: string,
- @Query() subDistrict?: string,
+ @Query() businessTypeId?: string,
+ @Query() provinceId?: string,
+ @Query() districtId?: string,
+ @Query() subDistrictId?: string,
) {
const where = {
OR: queryOrNot(query, [
@@ -199,27 +199,27 @@ export class CustomerController extends Controller {
branch: {
some: {
AND: [
- businessType
+ businessTypeId
? {
- OR: [{ businessType: { id: businessType } }],
+ OR: [{ businessType: { id: businessTypeId } }],
}
: {},
- province
+ provinceId
? {
- OR: [{ province: { id: province } }],
+ OR: [{ province: { id: provinceId } }],
}
: {},
- district
+ districtId
? {
- OR: [{ district: { id: district } }],
+ OR: [{ district: { id: districtId } }],
}
: {},
- subDistrict
+ subDistrictId
? {
- OR: [{ subDistrict: { id: subDistrict } }],
+ OR: [{ subDistrict: { id: subDistrictId } }],
}
: {},
],
@@ -630,10 +630,10 @@ export class CustomerExportController extends CustomerController {
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
- @Query() businessType?: string,
- @Query() province?: string,
- @Query() district?: string,
- @Query() subDistrict?: string,
+ @Query() businessTypeId?: string,
+ @Query() provinceId?: string,
+ @Query() districtId?: string,
+ @Query() subDistrictId?: string,
) {
const ret = await this.list(
req,
@@ -647,10 +647,10 @@ export class CustomerExportController extends CustomerController {
activeBranchOnly,
startDate,
endDate,
- businessType,
- province,
- district,
- subDistrict,
+ businessTypeId,
+ provinceId,
+ districtId,
+ subDistrictId,
);
this.setHeader("Content-Type", "text/csv");
From be3c6405c685d8f727af21afb1ef45b0b968074b Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 18 Sep 2025 10:15:32 +0700
Subject: [PATCH 34/54] feat: export product
---
src/controllers/04-product-controller.ts | 41 ++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts
index c1c40d8..612275e 100644
--- a/src/controllers/04-product-controller.ts
+++ b/src/controllers/04-product-controller.ts
@@ -31,6 +31,7 @@ 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",
@@ -673,3 +674,43 @@ 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 });
+ }
+}
From a426e18025b280acbcbdb8f3a8004d199eb22bc6 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 18 Sep 2025 16:55:34 +0700
Subject: [PATCH 35/54] fix: wrong number
---
src/services/flowaccount.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 9450b02..32d5c88 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -186,8 +186,6 @@ const flowAccountAPI = {
data.paymentDate = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`;
} */
- console.log(JSON.stringify(data, null, 2));
-
const res = await fetch(
api + "/upgrade/receipts/inline" + (withPayment ? "/with-payment" : ""),
{
@@ -338,7 +336,7 @@ const flowAccount = {
subTotal:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
- : quotation.totalPrice,
+ : quotation.totalPrice - quotation.totalDiscount - quotation.vatExcluded,
totalAfterDiscount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
From 7858291ae50e1888ff59ca912c3e79dfcc8625b3 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 18 Sep 2025 17:04:40 +0700
Subject: [PATCH 36/54] fix: receipt flow account
---
src/services/flowaccount.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 32d5c88..d004c66 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -334,9 +334,17 @@ const flowAccount = {
discountAmount: quotation.totalDiscount,
subTotal:
+ quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ ? 0
+ : quotation.totalPrice,
+ vatableAmount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
: quotation.totalPrice - quotation.totalDiscount - quotation.vatExcluded,
+ exemptAmount:
+ quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ ? 0
+ : quotation.vatExcluded,
totalAfterDiscount:
quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
? 0
From 9ef006c860eedef5d1bb536e03ebe4ee7b9387de Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Thu, 18 Sep 2025 17:12:02 +0700
Subject: [PATCH 37/54] refactor: handle flow account installments
---
src/services/flowaccount.ts | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index d004c66..2b4b706 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -334,23 +334,38 @@ const flowAccount = {
discountAmount: quotation.totalDiscount,
subTotal:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ quotation.payCondition === "BillSplitCustom" ||
+ quotation.payCondition === "BillSplit" ||
+ quotation.payCondition === "SplitCustom" ||
+ quotation.payCondition === "Split"
? 0
: quotation.totalPrice,
vatableAmount:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ quotation.payCondition === "BillSplitCustom" ||
+ quotation.payCondition === "BillSplit" ||
+ quotation.payCondition === "SplitCustom" ||
+ quotation.payCondition === "Split"
? 0
: quotation.totalPrice - quotation.totalDiscount - quotation.vatExcluded,
exemptAmount:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ quotation.payCondition === "BillSplitCustom" ||
+ quotation.payCondition === "BillSplit" ||
+ quotation.payCondition === "SplitCustom" ||
+ quotation.payCondition === "Split"
? 0
: quotation.vatExcluded,
totalAfterDiscount:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ quotation.payCondition === "BillSplitCustom" ||
+ quotation.payCondition === "BillSplit" ||
+ quotation.payCondition === "SplitCustom" ||
+ quotation.payCondition === "Split"
? 0
: quotation.finalPrice,
vatAmount:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
+ quotation.payCondition === "BillSplitCustom" ||
+ quotation.payCondition === "BillSplit" ||
+ quotation.payCondition === "SplitCustom" ||
+ quotation.payCondition === "Split"
? 0
: quotation.vat,
grandTotal:
From a33983c530ffcd6c6952b4ba0061d49203a17d9d Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Fri, 19 Sep 2025 09:11:21 +0700
Subject: [PATCH 38/54] fix: transaction ended before create
---
src/controllers/04-product-controller.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts
index 612275e..d58aca3 100644
--- a/src/controllers/04-product-controller.ts
+++ b/src/controllers/04-product-controller.ts
@@ -304,7 +304,7 @@ export class ProductController extends Controller {
const listId = await flowAccount.createProducts(body);
- return await prisma.product.create({
+ return await tx.product.create({
include: {
createdBy: true,
updatedBy: true,
From 95ee32fc571c96d81a21c66938e666e50e582645 Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Fri, 19 Sep 2025 11:12:03 +0700
Subject: [PATCH 39/54] feat: response branch api
---
src/controllers/01-branch-controller.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/controllers/01-branch-controller.ts b/src/controllers/01-branch-controller.ts
index a09e24b..943413b 100644
--- a/src/controllers/01-branch-controller.ts
+++ b/src/controllers/01-branch-controller.ts
@@ -387,6 +387,14 @@ 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) {
From a9201f715adecac609f09aec195b0f4506b450d4 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 19 Sep 2025 15:35:14 +0700
Subject: [PATCH 40/54] =?UTF-8?q?fix:=20missing=20product=20code,=20buyPri?=
=?UTF-8?q?ce=20when=20create=20and=20edit=20and=20add=20(=E0=B8=A3?=
=?UTF-8?q?=E0=B8=B2=E0=B8=84=E0=B8=B2=E0=B8=95=E0=B8=B1=E0=B8=A7=E0=B9=81?=
=?UTF-8?q?=E0=B8=97=E0=B8=99)=20for=20agentPrice=20product=20name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/controllers/04-product-controller.ts | 19 ++++++++-
src/services/flowaccount.ts | 54 +++++++++++++-----------
2 files changed, 47 insertions(+), 26 deletions(-)
diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts
index d58aca3..512fff8 100644
--- a/src/controllers/04-product-controller.ts
+++ b/src/controllers/04-product-controller.ts
@@ -78,6 +78,7 @@ type ProductCreate = {
type ProductUpdate = {
status?: "ACTIVE" | "INACTIVE";
+ code?: string;
name?: string;
detail?: string;
process?: number;
@@ -302,7 +303,10 @@ export class ProductController extends Controller {
update: { value: { increment: 1 } },
});
- const listId = await flowAccount.createProducts(body);
+ const listId = await flowAccount.createProducts(
+ `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
+ body,
+ );
return await tx.product.create({
include: {
@@ -390,10 +394,21 @@ export class ProductController extends Controller {
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,
- body,
+ mergedBody,
);
} else {
throw notFoundError("FlowAccountProductId");
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 2b4b706..6f131fe 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -455,19 +455,22 @@ const flowAccount = {
},
// flowAccount POST create Product
- async createProducts(body: JsonObject) {
+ async createProducts(code: String, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
productStructureType: null,
- type: "3",
+ type: 3,
+ code: `${code}`,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
- unitName: "Unit",
+ buyPrice: body.serviceCharge,
+ buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
+ buyDescription: body.detail,
};
- const createProduct = async (price: any, vatIncluded: boolean) => {
+ const createProduct = async (name: string, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(api + "/products", {
method: "POST",
@@ -477,13 +480,14 @@ const flowAccount = {
},
body: JSON.stringify({
...commonBody,
+ name: name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) {
- throw new Error(`Request failed with status ${res.status}`);
+ throw new Error(`Request failed with status ${res.status} ${res}`);
}
let json: any = null;
@@ -501,8 +505,12 @@ const flowAccount = {
};
const [sellId, agentId] = await Promise.all([
- createProduct(body.price, /true/.test(`${body.vatIncluded}`)),
- createProduct(body.agentPrice, /true/.test(`${body.agentPriceVatIncluded}`)),
+ createProduct(`${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
+ createProduct(
+ `${body.name} (ราคาตัวแทน)`,
+ body.agentPrice,
+ /true/.test(`${body.agentPriceVatIncluded}`),
+ ),
]);
return {
@@ -521,15 +529,16 @@ const flowAccount = {
const commonBody = {
productStructureType: null,
- type: "3",
+ type: 3,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
- unitName: "Unit",
- categoryName: "Car",
+ buyPrice: body.serviceCharge,
+ buyVatType: body.serviceChargeVatIncluded ? 1 : 3,
+ buyDescription: body.detail,
};
- const editProduct = async (id: String, price: any, vatIncluded: boolean) => {
+ const editProduct = async (id: String, name: String, price: any, vatIncluded: boolean) => {
try {
const res = await fetch(api + `/products/${id}`, {
method: "PUT",
@@ -539,13 +548,14 @@ const flowAccount = {
},
body: JSON.stringify({
...commonBody,
+ name: name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
if (!res.ok) {
- throw new Error(`Request failed with status ${res.status}`);
+ throw new Error(`Request failed with status ${res.status} ${res}`);
}
let json: any = null;
@@ -562,19 +572,15 @@ const flowAccount = {
}
};
- const [sellId, agentId] = await Promise.all([
- editProduct(sellPriceId, body.price, /true/.test(`${body.vatIncluded}`)),
- editProduct(agentPriceId, body.agentPrice, /true/.test(`${body.agentPriceVatIncluded}`)),
+ await Promise.all([
+ editProduct(sellPriceId, `${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
+ editProduct(
+ agentPriceId,
+ `${body.name} (ราคาตัวแทน)`,
+ body.agentPrice,
+ /true/.test(`${body.agentPriceVatIncluded}`),
+ ),
]);
-
- return {
- ok: !!(agentId && sellId),
- status: agentId && sellId ? 200 : 500,
- data: {
- productIdSellPrice: sellId,
- productIdAgentPrice: agentId,
- },
- };
},
// flowAccount DELETE Product
From 334fb57b46b344aca8e14cf70dc803be6f7ad936 Mon Sep 17 00:00:00 2001
From: HAM
Date: Fri, 19 Sep 2025 16:09:21 +0700
Subject: [PATCH 41/54] refactor: flowaccount move product code to front for
product name
---
src/services/flowaccount.ts | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 6f131fe..df40d27 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -461,7 +461,6 @@ const flowAccount = {
const commonBody = {
productStructureType: null,
type: 3,
- code: `${code}`,
name: body.name,
sellDescription: body.detail,
sellVatType: 3,
@@ -505,9 +504,9 @@ const flowAccount = {
};
const [sellId, agentId] = await Promise.all([
- createProduct(`${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
+ createProduct(`${code} ${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
createProduct(
- `${body.name} (ราคาตัวแทน)`,
+ `${code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
@@ -573,10 +572,15 @@ const flowAccount = {
};
await Promise.all([
- editProduct(sellPriceId, `${body.name}`, body.price, /true/.test(`${body.vatIncluded}`)),
+ editProduct(
+ sellPriceId,
+ `${body.code} ${body.name}`,
+ body.price,
+ /true/.test(`${body.vatIncluded}`),
+ ),
editProduct(
agentPriceId,
- `${body.name} (ราคาตัวแทน)`,
+ `${body.code} ${body.name} (ราคาตัวแทน)`,
body.agentPrice,
/true/.test(`${body.agentPriceVatIncluded}`),
),
From 3454e462120e3fa87dd40292e736cf872a2783ef Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Fri, 19 Sep 2025 16:17:56 +0700
Subject: [PATCH 42/54] feat: flowaccount handle installments
---
src/services/flowaccount.ts | 103 ++++++++++++++++++------------------
1 file changed, 51 insertions(+), 52 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index df40d27..0c46f50 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -289,13 +289,55 @@ const flowAccount = {
const quotation = data.quotation;
const customer = quotation.customerBranch;
- const product =
+
+ const summary = {
+ subTotal: 0,
+ discountAmount: 0,
+ vatableAmount: 0,
+ exemptAmount: 0,
+ vatAmount: 0,
+ grandTotal: 0,
+ };
+
+ const products = (
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 || "-",
@@ -333,45 +375,12 @@ const flowAccount = {
discounPercentage: 0,
discountAmount: quotation.totalDiscount,
- subTotal:
- quotation.payCondition === "BillSplitCustom" ||
- quotation.payCondition === "BillSplit" ||
- quotation.payCondition === "SplitCustom" ||
- quotation.payCondition === "Split"
- ? 0
- : quotation.totalPrice,
- vatableAmount:
- quotation.payCondition === "BillSplitCustom" ||
- quotation.payCondition === "BillSplit" ||
- quotation.payCondition === "SplitCustom" ||
- quotation.payCondition === "Split"
- ? 0
- : quotation.totalPrice - quotation.totalDiscount - quotation.vatExcluded,
- exemptAmount:
- quotation.payCondition === "BillSplitCustom" ||
- quotation.payCondition === "BillSplit" ||
- quotation.payCondition === "SplitCustom" ||
- quotation.payCondition === "Split"
- ? 0
- : quotation.vatExcluded,
- totalAfterDiscount:
- quotation.payCondition === "BillSplitCustom" ||
- quotation.payCondition === "BillSplit" ||
- quotation.payCondition === "SplitCustom" ||
- quotation.payCondition === "Split"
- ? 0
- : quotation.finalPrice,
- vatAmount:
- quotation.payCondition === "BillSplitCustom" ||
- quotation.payCondition === "BillSplit" ||
- quotation.payCondition === "SplitCustom" ||
- quotation.payCondition === "Split"
- ? 0
- : quotation.vat,
- grandTotal:
- quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom"
- ? data.installments.reduce((a, c) => a + c.amount, 0)
- : quotation.finalPrice,
+ subTotal: summary.subTotal,
+ totalAfterDiscount: summary.subTotal - summary.discountAmount,
+ vatableAmount: summary.vatableAmount,
+ exemptAmount: summary.exemptAmount,
+ vatAmount: summary.vatAmount,
+ grandTotal: summary.grandTotal,
remarks: htmlToText(
convertTemplate(quotation.remark ?? "", {
@@ -389,17 +398,7 @@ const flowAccount = {
},
}),
),
- items: product.map((v) => ({
- type: ProductAndServiceType.ProductNonInv,
- name: v.product.name,
- pricePerUnit: precisionRound(v.pricePerUnit),
- quantity: v.amount,
- discountAmount: v.discount,
- total:
- precisionRound(v.pricePerUnit * (1 + (v.vat > 0 ? VAT_DEFAULT : 0))) * v.amount -
- (v.discount ?? 0),
- vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100),
- })),
+ items: products,
};
return await flowAccountAPI.createReceipt(payload, false);
From 5dc88c22dcf37f5e48df14be1e6e5751eade858b Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 29 Sep 2025 09:44:41 +0700
Subject: [PATCH 43/54] feat: delete created data when flow account create fail
one
---
src/services/flowaccount.ts | 52 ++++++++++++++++++++++++++-----------
1 file changed, 37 insertions(+), 15 deletions(-)
diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts
index 0c46f50..7faf492 100644
--- a/src/services/flowaccount.ts
+++ b/src/services/flowaccount.ts
@@ -454,7 +454,7 @@ const flowAccount = {
},
// flowAccount POST create Product
- async createProducts(code: String, body: JsonObject) {
+ async createProducts(code: string, body: JsonObject) {
const { token } = await flowAccountAPI.auth();
const commonBody = {
@@ -470,7 +470,7 @@ const flowAccount = {
const createProduct = async (name: string, price: any, vatIncluded: boolean) => {
try {
- const res = await fetch(api + "/products", {
+ const res = await fetch(`${api}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -478,22 +478,17 @@ const flowAccount = {
},
body: JSON.stringify({
...commonBody,
- name: name,
+ name,
sellPrice: price,
sellVatType: vatIncluded ? 1 : 3,
}),
});
- if (!res.ok) {
- throw new Error(`Request failed with status ${res.status} ${res}`);
- }
+ if (!res.ok) throw new Error(`HTTP ${res.status}: Failed to create product`);
- let json: any = null;
- try {
- json = await res.json();
- } catch {
- throw new Error("Response is not valid JSON");
- }
+ const json = await res.json().catch(() => {
+ throw new Error("Invalid JSON response from FlowAccount API");
+ });
return json?.data?.list?.[0]?.id ?? null;
} catch (err) {
@@ -502,7 +497,18 @@ const flowAccount = {
}
};
- const [sellId, agentId] = await Promise.all([
+ 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} (ราคาตัวแทน)`,
@@ -511,9 +517,25 @@ const flowAccount = {
),
]);
+ 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: !!(agentId && sellId),
- status: agentId && sellId ? 200 : 500,
+ ok: true,
+ status: 200,
data: {
productIdSellPrice: sellId,
productIdAgentPrice: agentId,
From 78669ed7aee2d0b2a03cab425f62397cd692e3f0 Mon Sep 17 00:00:00 2001
From: HAM
Date: Tue, 14 Oct 2025 16:19:37 +0700
Subject: [PATCH 44/54] feat: check invalid data for create taskOrder
---
src/controllers/07-task-controller.ts | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts
index ad97be0..cf7b402 100644
--- a/src/controllers/07-task-controller.ts
+++ b/src/controllers/07-task-controller.ts
@@ -77,6 +77,7 @@ export class TaskController extends Controller {
by: ["taskOrderStatus"],
_count: true,
});
+
return task.reduce>(
(a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }),
{
@@ -264,6 +265,12 @@ export class TaskController extends Controller {
taskProduct?: { productId: string; discount?: number }[];
},
) {
+ if (!body.taskList || !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: {
@@ -311,10 +318,10 @@ 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",
+ throw new httperror(
+ httpstatus.precondition_failed,
+ "all request work to issue task order must be in ready state.",
+ "requestworkmustready",
);
}
await tx.institution.updateMany({
From 16c4c64c899b3666cb15f5d20b05606c31075732 Mon Sep 17 00:00:00 2001
From: net
Date: Tue, 14 Oct 2025 17:12:00 +0700
Subject: [PATCH 45/54] refactor: handle code error
---
src/controllers/07-task-controller.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts
index cf7b402..02def1e 100644
--- a/src/controllers/07-task-controller.ts
+++ b/src/controllers/07-task-controller.ts
@@ -265,7 +265,7 @@ export class TaskController extends Controller {
taskProduct?: { productId: string; discount?: number }[];
},
) {
- if (!body.taskList || !body.registeredBranchId)
+ if (body.taskList.length < 1 || !body.registeredBranchId)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Your created invalid task order",
@@ -318,8 +318,8 @@ export class TaskController extends Controller {
});
if (updated.count !== taskList.length) {
- throw new httperror(
- httpstatus.precondition_failed,
+ throw new HttpError(
+ HttpStatus.PRECONDITION_FAILED,
"all request work to issue task order must be in ready state.",
"requestworkmustready",
);
From cef26278ba7f58cc7f200778f05dced5c467fac7 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 13:15:31 +0700
Subject: [PATCH 46/54] fix: missing prisma query take and skip
---
src/controllers/08-credit-note-controller.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/controllers/08-credit-note-controller.ts b/src/controllers/08-credit-note-controller.ts
index 7cd029e..a8e8767 100644
--- a/src/controllers/08-credit-note-controller.ts
+++ b/src/controllers/08-credit-note-controller.ts
@@ -156,7 +156,6 @@ export class CreditNoteController extends Controller {
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
- @Body() body?: {},
) {
const where = {
OR: queryOrNot(query, [
@@ -218,6 +217,8 @@ export class CreditNoteController extends Controller {
const [result, total] = await prisma.$transaction([
prisma.creditNote.findMany({
where,
+ take: pageSize,
+ skip: (page - 1) * pageSize,
include: {
quotation: {
include: {
From e54f62a5b3334bca74223a2168261c59f545752e Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 13:39:04 +0700
Subject: [PATCH 47/54] fix: prisma v7 config
---
prisma.config.ts | 7 +++++++
src/db.ts | 1 +
2 files changed, 8 insertions(+)
create mode 100644 prisma.config.ts
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 0000000..69604f1
--- /dev/null
+++ b/prisma.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+ migrations: {
+ path: "prisma/migrations",
+ },
+});
diff --git a/src/db.ts b/src/db.ts
index 1e2206b..0402c07 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -4,6 +4,7 @@ import kyselyExtension from "prisma-extension-kysely";
import type { DB } from "./generated/kysely/types";
const prisma = new PrismaClient({
+ datasourceUrl: process.env.DATABASE_URL,
errorFormat: process.env.NODE_ENV === "production" ? "minimal" : "pretty",
}).$extends(
kyselyExtension({
From 84b9ddcd2b0fb133a17b310822b6fcdf42958618 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 13:41:00 +0700
Subject: [PATCH 48/54] fix: prisma v7 config in schema.prisma
---
prisma/schema.prisma | 1 -
1 file changed, 1 deletion(-)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2f25bb2..b8f518b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -10,7 +10,6 @@ generator kysely {
datasource db {
provider = "postgresql"
- url = env("DATABASE_URL")
}
model Notification {
From f0a106e5fee18c2b7f3c3d7df979ab9ed6e571b0 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 13:48:10 +0700
Subject: [PATCH 49/54] fix: use prisma v6 config
---
package.json | 6 ++++--
prisma/schema.prisma | 1 +
src/db.ts | 1 -
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 92e1d51..69799c0 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"@vitest/ui": "^3.1.4",
"nodemon": "^3.1.9",
"prettier": "^3.4.2",
- "prisma": "^6.16.2",
+ "prisma": "6.16.2",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2",
@@ -40,13 +40,14 @@
"dependencies": {
"@elastic/elasticsearch": "^8.17.0",
"@fast-csv/parse": "^5.0.2",
- "@prisma/client": "^6.16.2",
+ "@prisma/client": "6.16.2",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
"@types/html-to-text": "^9.0.4",
"canvas": "^3.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",
@@ -62,6 +63,7 @@
"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",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index b8f518b..2f25bb2 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -10,6 +10,7 @@ generator kysely {
datasource db {
provider = "postgresql"
+ url = env("DATABASE_URL")
}
model Notification {
diff --git a/src/db.ts b/src/db.ts
index 0402c07..1e2206b 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -4,7 +4,6 @@ import kyselyExtension from "prisma-extension-kysely";
import type { DB } from "./generated/kysely/types";
const prisma = new PrismaClient({
- datasourceUrl: process.env.DATABASE_URL,
errorFormat: process.env.NODE_ENV === "production" ? "minimal" : "pretty",
}).$extends(
kyselyExtension({
From 8909a763c9fe10c8da0f80a3c46a99bc566f3c88 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 13:51:54 +0700
Subject: [PATCH 50/54] chore: remove unuse file
---
prisma.config.ts | 7 -------
1 file changed, 7 deletions(-)
delete mode 100644 prisma.config.ts
diff --git a/prisma.config.ts b/prisma.config.ts
deleted file mode 100644
index 69604f1..0000000
--- a/prisma.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { defineConfig } from "prisma/config";
-
-export default defineConfig({
- migrations: {
- path: "prisma/migrations",
- },
-});
From c11fed98325507faefde5af1bbff724943ab9cad Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 14:10:11 +0700
Subject: [PATCH 51/54] fix: lock prisma to v6 and fix lockfile
---
pnpm-lock.yaml | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5c7f3d7..d724159 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,7 +15,7 @@ importers:
specifier: ^5.0.2
version: 5.0.2
'@prisma/client':
- specifier: ^6.16.2
+ specifier: 6.16.2
version: 6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2)
'@scalar/express-api-reference':
specifier: ^0.4.182
@@ -35,6 +35,9 @@ importers:
cron:
specifier: ^3.3.1
version: 3.3.1
+ csv-parse:
+ specifier: ^6.1.0
+ version: 6.1.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
@@ -80,6 +83,9 @@ importers:
nodemailer:
specifier: ^6.10.0
version: 6.10.0
+ pnpm:
+ specifier: ^10.18.3
+ version: 10.28.0
prisma-extension-kysely:
specifier: ^3.0.0
version: 3.0.0(@prisma/client@6.16.2(prisma@6.16.2(typescript@5.7.2))(typescript@5.7.2))
@@ -133,7 +139,7 @@ importers:
specifier: ^3.4.2
version: 3.4.2
prisma:
- specifier: ^6.16.2
+ specifier: 6.16.2
version: 6.16.2(typescript@5.7.2)
prisma-kysely:
specifier: ^1.8.0
@@ -1283,6 +1289,9 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
+ csv-parse@6.1.0:
+ resolution: {integrity: sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==}
+
data-view-buffer@1.0.1:
resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
engines: {node: '>= 0.4'}
@@ -2656,6 +2665,11 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+ pnpm@10.28.0:
+ resolution: {integrity: sha512-Bd9x0UIfITmeBT/eVnzqNNRG+gLHZXFEG/wceVbpjjYwiJgtlARl/TRIDU2QoGaLwSNi+KqIAApk6D0LDke+SA==}
+ engines: {node: '>=18.12'}
+ hasBin: true
+
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
@@ -4924,6 +4938,8 @@ snapshots:
crypto-random-string@2.0.0: {}
+ csv-parse@6.1.0: {}
+
data-view-buffer@1.0.1:
dependencies:
call-bind: 1.0.8
@@ -6432,6 +6448,8 @@ snapshots:
exsolve: 1.0.7
pathe: 2.0.3
+ pnpm@10.28.0: {}
+
possible-typed-array-names@1.0.0: {}
postcss@8.5.3:
From a10626e75627d29548b38f0e8d455b4ba564f414 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 14:32:29 +0700
Subject: [PATCH 52/54] update: entrypoint.sh
---
entrypoint.sh | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/entrypoint.sh b/entrypoint.sh
index 4d9a5e6..df5f60f 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,4 +1,8 @@
#!/bin/sh
+set -e
-pnpm prisma migrate deploy
-pnpm run start
+echo "Running prisma migrations..."
+npx prisma migrate deploy
+
+echo "Starting server..."
+node ./dist/app.js
From 5d3997343fb007fd4e22223d947b0d4aced5e992 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 14:52:16 +0700
Subject: [PATCH 53/54] fix: Dockerfile
---
Dockerfile | 36 ++++++++++++++++++++----------------
1 file changed, 20 insertions(+), 16 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 7b7967d..3ff374d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,32 +3,36 @@ FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
-RUN corepack enable
-RUN apt-get update && apt-get install -y openssl fontconfig
-RUN fc-cache -f -v
-RUN pnpm i -g prisma prisma-kysely
+RUN corepack enable \
+ && corepack prepare pnpm@9.15.0 --activate
+
+RUN apt-get update \
+ && apt-get install -y openssl fontconfig \
+ && fc-cache -f -v \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app
-
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 --mount=type=cache,id=pnpm,target=/pnpm/store \
+ pnpm install --frozen-lockfile
+RUN pnpm exec prisma generate
RUN pnpm run build
-FROM base AS prod
+FROM base AS deps
+RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
+ pnpm install --prod --frozen-lockfile
-ENV NODE_ENV="production"
+FROM node:23-slim AS prod
+ENV NODE_ENV=production
+WORKDIR /app
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
+COPY --from=build /app/prisma /app/prisma
+COPY --from=build /app/static /app/static
+COPY entrypoint.sh .
+RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
From 426ffb27a7a9055c6515bc6dae7a95b60cf674c6 Mon Sep 17 00:00:00 2001
From: HAM
Date: Mon, 12 Jan 2026 15:05:21 +0700
Subject: [PATCH 54/54] fix: update Dockerfile
---
Dockerfile | 44 ++++++++++++++------------------------------
entrypoint.sh | 8 ++------
2 files changed, 16 insertions(+), 36 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 3ff374d..f29dc9a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,38 +1,22 @@
-FROM node:23-slim AS base
+FROM node:20-slim
-ENV PNPM_HOME="/pnpm"
-ENV PATH="$PNPM_HOME:$PATH"
-
-RUN corepack enable \
- && corepack prepare pnpm@9.15.0 --activate
-
-RUN apt-get update \
- && apt-get install -y openssl fontconfig \
- && fc-cache -f -v \
- && rm -rf /var/lib/apt/lists/*
+RUN apt-get update -y \
+ && apt-get install -y openssl \
+ && npm install -g pnpm \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app
+
+COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile
+
COPY . .
-FROM base AS build
-RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
- pnpm install --frozen-lockfile
-RUN pnpm exec prisma generate
+RUN pnpm prisma generate
RUN pnpm run build
-FROM base AS deps
-RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
- pnpm install --prod --frozen-lockfile
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
-FROM node:23-slim AS prod
-ENV NODE_ENV=production
-WORKDIR /app
-
-COPY --from=deps /app/node_modules /app/node_modules
-COPY --from=build /app/dist /app/dist
-COPY --from=build /app/prisma /app/prisma
-COPY --from=build /app/static /app/static
-COPY entrypoint.sh .
-
-RUN chmod +x ./entrypoint.sh
-ENTRYPOINT ["./entrypoint.sh"]
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/entrypoint.sh b/entrypoint.sh
index df5f60f..4d9a5e6 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,8 +1,4 @@
#!/bin/sh
-set -e
-echo "Running prisma migrations..."
-npx prisma migrate deploy
-
-echo "Starting server..."
-node ./dist/app.js
+pnpm prisma migrate deploy
+pnpm run start