From 9f9c9aa80d1ed13a734a4835196763645a3a4830 Mon Sep 17 00:00:00 2001 From: x2Skyz Date: Mon, 17 Nov 2025 09:03:36 +0700 Subject: [PATCH] =?UTF-8?q?-=E0=B8=95=E0=B9=89=E0=B8=99=E0=B9=81=E0=B8=9A?= =?UTF-8?q?=E0=B8=9A=20=E0=B9=82=E0=B8=84=E0=B8=A3=E0=B8=87=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87=20=E0=B9=84=E0=B8=9F?= =?UTF-8?q?=E0=B8=A5=E0=B9=8C=20API=20=E0=B9=80=E0=B8=AA=E0=B9=89=E0=B8=99?= =?UTF-8?q?=20/api/ttc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exthernal-ttc-api/.env | 28 +++ exthernal-ttc-api/.vscode/launch.json | 29 +++ exthernal-ttc-api/package.json | 22 +++ exthernal-ttc-api/src/app.js | 32 ++++ exthernal-ttc-api/src/config/db.js | 13 ++ .../controllers/accountingSearchController.js | 45 +++++ .../controllers/accountingSetupController.js | 54 ++++++ .../controllers/accountingSumController.js | 142 ++++++++++++++ exthernal-ttc-api/src/middlewares/auth.js | 14 ++ .../src/middlewares/responseHandler.js | 20 ++ exthernal-ttc-api/src/middlewares/validate.js | 24 +++ .../src/middlewares/verifyEmailHandler.js | 37 ++++ exthernal-ttc-api/src/routes/route.js | 59 ++++++ .../src/services/accountingSearchService.js | 27 +++ .../src/services/accountingSetupService.js | 27 +++ .../src/services/accountingSumService.js | 44 +++++ exthernal-ttc-api/src/share/generalservice.js | 177 ++++++++++++++++++ exthernal-ttc-api/src/utils/errorList.js | 40 ++++ exthernal-ttc-api/src/utils/mailer.js | 62 ++++++ exthernal-ttc-api/src/utils/oftenError.js | 41 ++++ exthernal-ttc-api/src/utils/otp.js | 3 + exthernal-ttc-api/src/utils/redis.js | 28 +++ exthernal-ttc-api/src/utils/response.js | 24 +++ exthernal-ttc-api/src/utils/token.js | 16 ++ exthernal-ttc-api/src/utils/trim.js | 11 ++ 25 files changed, 1019 insertions(+) create mode 100644 exthernal-ttc-api/.env create mode 100644 exthernal-ttc-api/.vscode/launch.json create mode 100644 exthernal-ttc-api/package.json create mode 100644 exthernal-ttc-api/src/app.js create mode 100644 exthernal-ttc-api/src/config/db.js create mode 100644 exthernal-ttc-api/src/controllers/accountingSearchController.js create mode 100644 exthernal-ttc-api/src/controllers/accountingSetupController.js create mode 100644 exthernal-ttc-api/src/controllers/accountingSumController.js create mode 100644 exthernal-ttc-api/src/middlewares/auth.js create mode 100644 exthernal-ttc-api/src/middlewares/responseHandler.js create mode 100644 exthernal-ttc-api/src/middlewares/validate.js create mode 100644 exthernal-ttc-api/src/middlewares/verifyEmailHandler.js create mode 100644 exthernal-ttc-api/src/routes/route.js create mode 100644 exthernal-ttc-api/src/services/accountingSearchService.js create mode 100644 exthernal-ttc-api/src/services/accountingSetupService.js create mode 100644 exthernal-ttc-api/src/services/accountingSumService.js create mode 100644 exthernal-ttc-api/src/share/generalservice.js create mode 100644 exthernal-ttc-api/src/utils/errorList.js create mode 100644 exthernal-ttc-api/src/utils/mailer.js create mode 100644 exthernal-ttc-api/src/utils/oftenError.js create mode 100644 exthernal-ttc-api/src/utils/otp.js create mode 100644 exthernal-ttc-api/src/utils/redis.js create mode 100644 exthernal-ttc-api/src/utils/response.js create mode 100644 exthernal-ttc-api/src/utils/token.js create mode 100644 exthernal-ttc-api/src/utils/trim.js diff --git a/exthernal-ttc-api/.env b/exthernal-ttc-api/.env new file mode 100644 index 0000000..b94ec4d --- /dev/null +++ b/exthernal-ttc-api/.env @@ -0,0 +1,28 @@ +#project +PJ_NAME=exthernal-ttc-api + +# database +PG_HOST=localhost +PG_USER=postgres +PG_PASS=123456 +PG_DB=ttc +PG_PORT=5432 + +# EMAIL +SMTP_USER=lalisakuty@gmail.com +SMTP_PASS=lurl pckw qugk tzob + +# REDIS +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +OTP_TTL_SECONDS=300 + +# JWT-TOKENS +JWT_SECRET=5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80 + +# DEV_HINT +DEVHINT=true +DEVHINT_LEVEL=3 + +#PORT +PORT=1011 diff --git a/exthernal-ttc-api/.vscode/launch.json b/exthernal-ttc-api/.vscode/launch.json new file mode 100644 index 0000000..635dcca --- /dev/null +++ b/exthernal-ttc-api/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run API (Nodemon Debug)", + "type": "node", + "request": "launch", + "runtimeExecutable": "nodemon", + "program": "${workspaceFolder}/src/app.js", + "restart": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "envFile": "${workspaceFolder}/.env", + "cwd": "${workspaceFolder}", + "runtimeArgs": ["--inspect=9229"], + "skipFiles": ["/**"], + // "env": { + // "PJ_NAME": "exthernal-mobile-api", + // "PG_HOST": "localhost", + // "PG_USER": "postgres", + // "PG_PASS": "1234", + // "PG_DB": "postgres", + // "PG_PORT": "5432", + // "JWT_SECRET": "MY_SUPER_SECRET", + // "PORT": "4000" + // } + } + ] +} diff --git a/exthernal-ttc-api/package.json b/exthernal-ttc-api/package.json new file mode 100644 index 0000000..f7f1426 --- /dev/null +++ b/exthernal-ttc-api/package.json @@ -0,0 +1,22 @@ +{ + "name": "exthernal-mobile-api", + "version": "1.0.0", + "description": "External Mobile API following Nuttakit Controller Pattern vFinal", + "type": "module", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js" + }, + "author": "Nuttakit Pothong", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.19.2", + "pg": "^8.12.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} diff --git a/exthernal-ttc-api/src/app.js b/exthernal-ttc-api/src/app.js new file mode 100644 index 0000000..a483568 --- /dev/null +++ b/exthernal-ttc-api/src/app.js @@ -0,0 +1,32 @@ +import express from 'express' +import cors from 'cors' +import dotenv from 'dotenv' +import router from './routes/route.js' +import { globalResponseHandler } from './middlewares/responseHandler.js' + +dotenv.config() + +const app = express() +app.use(cors()) +app.use(express.json({ limit: '10mb' })) + +app.use(globalResponseHandler); + +app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + console.error('🟥 Invalid JSON Received:', err.message) + return res.status(400).json({ + code: "400", + message: "Invalid JSON format", + message_th: "โครงสร้าง JSON ไม่ถูกต้อง", + data: [] + }) + } + next() +}) + +app.use('/api/ttc', router) + +app.listen(process.env.PORT, () => { + console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`) +}) diff --git a/exthernal-ttc-api/src/config/db.js b/exthernal-ttc-api/src/config/db.js new file mode 100644 index 0000000..07272d0 --- /dev/null +++ b/exthernal-ttc-api/src/config/db.js @@ -0,0 +1,13 @@ +import pkg from 'pg' +import dotenv from 'dotenv' + +dotenv.config() +const { Pool } = pkg + +export const connection = new Pool({ + host: process.env.PG_HOST, + user: process.env.PG_USER, + password: process.env.PG_PASS, + database: process.env.PG_DB, + port: process.env.PG_PORT, +}) diff --git a/exthernal-ttc-api/src/controllers/accountingSearchController.js b/exthernal-ttc-api/src/controllers/accountingSearchController.js new file mode 100644 index 0000000..6890fc8 --- /dev/null +++ b/exthernal-ttc-api/src/controllers/accountingSearchController.js @@ -0,0 +1,45 @@ +import { AccountingSearchService } from '../services/accountingSearchService.js' +import { sendError } from '../utils/response.js' +// import { OftenError } from '../utils/oftenError.js' +import { GeneralService } from '../share/generalservice.js'; +import { trim_all_array } from '../utils/trim.js' +import { verifyToken, generateToken } from '../utils/token.js' + +export class accountingSearch { + + constructor() { + this.generalService = new GeneralService(); + this.accountingSearchService = new AccountingSearchService(); + } + + async onNavigate(req, res) { + this.generalService.devhint(1, 'accountingSearch.js', 'onNavigate() start'); + let organization = req.body.organization; + const prommis = await this.onAccountingSearch(req, res, organization); + return prommis; + } + + async onAccountingSearch(req, res, database) { + let idx = -1 + let aryResult = [] + try { + // let username = req.body.request.username; + // let password = req.body.request.password; + let token = req.body.request.token; + const decoded = verifyToken(token); + + let id = decoded.id + let username = decoded.name + database = decoded.organization + + aryResult = await this.accountingSearchService.getAccountingSearch(database, id, username); // เช็คกับ db กลาง ส่ง jwttoken ออกมา + // this.generalService.devhint(1, 'accountingSearch.js', 'Login success'); + } catch (error) { + idx = 1; + } finally { + if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error'); + if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data'); + return aryResult + } + } +} diff --git a/exthernal-ttc-api/src/controllers/accountingSetupController.js b/exthernal-ttc-api/src/controllers/accountingSetupController.js new file mode 100644 index 0000000..2651508 --- /dev/null +++ b/exthernal-ttc-api/src/controllers/accountingSetupController.js @@ -0,0 +1,54 @@ +import { AccountingSetupService } from '../services/accountingSetupService.js' +import { sendError } from '../utils/response.js' +// import { OftenError } from '../utils/oftenError.js' +import { GeneralService } from '../share/generalservice.js'; +import { trim_all_array } from '../utils/trim.js' +import { verifyToken, generateToken } from '../utils/token.js' + +export class accountingSetup { + + constructor() { + this.generalService = new GeneralService(); + this.AccountingSetupService = new AccountingSetupService(); + } + + async onNavigate(req, res) { + this.generalService.devhint(1, 'accountingSetup.js', 'onNavigate() start'); + let organization = req.body.organization; + const prommis = await this.onAccountingSetup(req, res, organization); + return prommis; + } + + async onAccountingSetup(req, res, database) { + let idx = -1 + let result = [] + try { + // let username = req.body.request.username; + // let password = req.body.request.password; + let token = req.body.request.token; + const decoded = verifyToken(token); + + database = decoded.organization + + + result = await this.AccountingSetupService.getAccountingSetup(database); // เช็คกับ db กลาง ส่ง jwttoken ออกมา + // this.generalService.devhint(1, 'accountingSetup.js', 'Login success'); + } catch (error) { + idx = 1; + } finally { + if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error'); + if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials'); + // แยกกลุ่ม income / expense + let income = result.filter(item => item.dtltblcod === 'ACTCAT_INC').map(({ dtltblcod, ...rest }) => rest); + + let expense = result.filter(item => item.dtltblcod === 'ACTCAT_EXP').map(({ dtltblcod, ...rest }) => rest); + + let arydiy = { + income , + expense + }; + + return arydiy + } + } +} diff --git a/exthernal-ttc-api/src/controllers/accountingSumController.js b/exthernal-ttc-api/src/controllers/accountingSumController.js new file mode 100644 index 0000000..532d0fb --- /dev/null +++ b/exthernal-ttc-api/src/controllers/accountingSumController.js @@ -0,0 +1,142 @@ +import { AccountingSumService } from '../services/accountingSumService.js' +import { sendError } from '../utils/response.js' +import { GeneralService } from '../share/generalservice.js'; +import { trim_all_array } from '../utils/trim.js' +import { verifyToken, generateToken } from '../utils/token.js' + +export class accountingSum { + + constructor() { + this.generalService = new GeneralService(); + this.accountingSumService = new AccountingSumService(); + } + + async onNavigate(req, res) { + this.generalService.devhint(1, 'AccountingSum.js', 'onNavigate() start'); + let organization = req.body.organization; + const prommis = await this.onAccountingSum(req, res, organization); + return prommis; + } + + async onAccountingSum(req, res, database) { + let idx = -1 + let result = [] + var aryResult + try { + let token = req.body.request.token; + const decoded = verifyToken(token); + + let id = decoded.id + let username = decoded.name + database = decoded.organization + + result = await this.accountingSumService.getAccountingSum(database, id); // เช็คกับ db กลาง ส่ง jwttoken ออกมา + // if(result){ + // if(result.acttyp == 'e'){ + // // (ยังไม่มีการใช้งาน) + // } + // } + + } catch (error) { + idx = 1; + } finally { + if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error'); + if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials'); + + try { + // ✅ 1) เตรียม data สำหรับใช้คำนวณ + // ถ้า service คืนมาเป็น { code, message, data: [...] } + const data = Array.isArray(result) + ? result + : Array.isArray(result.data) + ? result.data + : []; + + // ถ้าไม่มีข้อมูลก็ไม่ต้องคำนวณ + if (!data.length) { + return result; + } + + // ✅ 2) แยก income / expense + const incomeList = data.filter(i => i.acttyp === 'i'); + const expenseList = data.filter(e => e.acttyp === 'e'); + + const totalIncome = incomeList.reduce((sum, i) => sum + parseFloat(i.actqty || 0), 0); + const totalExpense = expenseList.reduce((sum, e) => sum + parseFloat(e.actqty || 0), 0); + + const netProfit = totalIncome - totalExpense; + const profitRate = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0; + const adjustedProfitRate = profitRate + 1.9; + + // ✅ 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า) + var summary = { + totalIncome: totalIncome.toFixed(2), + totalExpense: totalExpense.toFixed(2), + netProfit: netProfit.toFixed(2), + profitRate: profitRate.toFixed(2) + ' %', + adjustedProfitRate: adjustedProfitRate.toFixed(2) + ' %', + period: '30 วัน' + }; + + // ✅ 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม) + // ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้ + // key = ชื่อหมวด (actcatnam หรือ code), value = color + const categoryColorMap = await this.accountingSumService.getCategoryColorMap(database); + // ตัวอย่างที่คาดหวังจาก service: + // { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... } + + // ✅ 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense + const expenseAgg = {}; + expenseList.forEach(row => { + const key = row.actcatnam; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod + const amount = parseFloat(row.actqty || 0); + expenseAgg[key] = (expenseAgg[key] || 0) + amount; + }); + + const incomeAgg = {}; + incomeList.forEach(row => { + const key = row.actcatnam; + const amount = parseFloat(row.actqty || 0); + incomeAgg[key] = (incomeAgg[key] || 0) + amount; + }); + + const expensePie = Object.entries(expenseAgg).map(([cat, value]) => { + const percent = totalExpense > 0 ? (value / totalExpense) * 100 : 0; + const color = categoryColorMap[cat] || '#CCCCCC'; // fallback สี default + return { + label: cat, + value: value.toFixed(2), + percent: percent.toFixed(2), + color + }; + }); + + const incomePie = Object.entries(incomeAgg).map(([cat, value]) => { + const percent = totalIncome > 0 ? (value / totalIncome) * 100 : 0; + const color = categoryColorMap[cat] || '#CCCCCC'; + return { + label: cat, + value: value.toFixed(2), + percent: percent.toFixed(2), + color + }; + }); + + // ✅ 6) แนบข้อมูล pie chart เข้า result + var pie = { + expense: expensePie, + income: incomePie + }; + + } catch (err) { + console.error('calculate summary/pie error:', err); + } + let arydiy = { + summary, + pie + } + + return arydiy; + } + } +} diff --git a/exthernal-ttc-api/src/middlewares/auth.js b/exthernal-ttc-api/src/middlewares/auth.js new file mode 100644 index 0000000..d99d548 --- /dev/null +++ b/exthernal-ttc-api/src/middlewares/auth.js @@ -0,0 +1,14 @@ +import { verifyToken } from '../utils/token.js' +import { sendError } from '../utils/response.js' + +export function authMiddleware(req, res, next) { + const authHeader = req.headers['authorization'] + const token = authHeader && authHeader.split(' ')[1] + if (!token) return sendError('ไม่พบ Token', 'Missing token', 401) + + const decoded = verifyToken(token) + if (!decoded) return sendError('Token ไม่ถูกต้อง', 'Invalid token', 403) + + req.user = decoded + next() +} diff --git a/exthernal-ttc-api/src/middlewares/responseHandler.js b/exthernal-ttc-api/src/middlewares/responseHandler.js new file mode 100644 index 0000000..ce05cab --- /dev/null +++ b/exthernal-ttc-api/src/middlewares/responseHandler.js @@ -0,0 +1,20 @@ +import { formatSuccessResponse } from '../utils/response.js' + +export function globalResponseHandler(req, res, next) { + const oldJson = res.json.bind(res) + + res.json = (data) => { + if (!data) return oldJson(formatSuccessResponse(null)) + + // ถ้า code ไม่ใช่ 200 → ตั้ง HTTP status ให้ตรงกับ code + if (data?.code && String(data.code) !== '200') { + res.status(Number(data.code) || 400) + return oldJson(data) + } + + res.status(200) + return oldJson(formatSuccessResponse(data)) + } + + next() +} diff --git a/exthernal-ttc-api/src/middlewares/validate.js b/exthernal-ttc-api/src/middlewares/validate.js new file mode 100644 index 0000000..e4993e3 --- /dev/null +++ b/exthernal-ttc-api/src/middlewares/validate.js @@ -0,0 +1,24 @@ +import { sendError } from '../utils/response.js' + +/** + * ✅ Middleware สำหรับตรวจสอบความถูกต้องของ JSON body + * ป้องกัน body-parser crash (SyntaxError) + */ +export function validateJsonFormat(err, req, res, next) { + if (err instanceof SyntaxError && 'body' in err) { + console.error('[Invalid JSON Format]', err.message) + return sendError('รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format') + } + next() +} + +// /** +// * ✅ ตรวจสอบ body/query/params ว่ามีค่า organization หรือไม่ +// */ +// export function validateRequest(req, res, next) { +// const { organization } = req.body || {} +// if (!organization) { +// return sendResponse(res, 400, 'ไม่พบค่า organization', 'Missing organization') +// } +// next() +// } diff --git a/exthernal-ttc-api/src/middlewares/verifyEmailHandler.js b/exthernal-ttc-api/src/middlewares/verifyEmailHandler.js new file mode 100644 index 0000000..caa253f --- /dev/null +++ b/exthernal-ttc-api/src/middlewares/verifyEmailHandler.js @@ -0,0 +1,37 @@ +import Redis from 'ioredis'; +import { GeneralService } from '../share/generalservice.js'; +// import { sendError } from './response.js'; + +export async function verifyEmailHandler(req, res) { + const redis = new Redis(); + const generalService = new GeneralService(); + + try { + const { email, token } = req.query; + const schema = req.body?.organization || 'nuttakit'; // 🧩 ใช้ schema ตาม org + const storedData = await redis.get(`verify:${email}`); + + if (!storedData) { + return res.status(400).send('ลิงก์หมดอายุหรือไม่ถูกต้อง'); + } + + const { fname, lname, hashedPwd, token: storedToken } = JSON.parse(storedData); + if (token !== storedToken) { + return res.status(400).send('Token ไม่ถูกต้อง'); + } + + let sql = ` + INSERT INTO ${schema}.usrmst (usrnam, usrthinam, usrthilstnam, usrpwd, usrrol) + VALUES ($1, $2, $3, $4, 'U') + `; + let param = [email, fname, lname, hashedPwd]; + await generalService.executeQueryParam(sql, param); + + await redis.del(`verify:${email}`); + + res.send(`

✅ ยืนยันอีเมลสำเร็จ บัญชีของคุณถูกสร้างแล้ว (${schema})

`); + } catch (error) { + console.error('❌ [Verify Email Error]', error); + res.status(500).send('เกิดข้อผิดพลาดในระบบ'); + } +} diff --git a/exthernal-ttc-api/src/routes/route.js b/exthernal-ttc-api/src/routes/route.js new file mode 100644 index 0000000..698eec4 --- /dev/null +++ b/exthernal-ttc-api/src/routes/route.js @@ -0,0 +1,59 @@ +import express from 'express' +import { accountingSetup } from '../controllers/accountingSetupController.js' +import { accountingSearch } from '../controllers/accountingSearchController.js' +import { accountingSum } from '../controllers/accountingSumController.js' + +// import { authMiddleware } from '../middlewares/auth.js' +// import { sendResponse } from '../utils/response.js' + +const router = express.Router() +const controller_accountingSetup_post = new accountingSetup() +const controller_accountingSearch_post = new accountingSearch() +const controller_accountingSum_post = new accountingSum() + + +router.post('/accountingSetup', async (req, res) => { + const result = await controller_accountingSetup_post.onNavigate(req, res) + if (result) return res.json(result) +}) + + +router.post('/accountingSearch', async (req, res) => { + const result = await controller_accountingSearch_post.onNavigate(req, res) + if (result) return res.json(result) +}) + +router.post('/accountingSum', async (req, res) => { + const result = await controller_accountingSum_post.onNavigate(req, res) + if (result) return res.json(result) +}) + + +// // =================================================== +// // 🔹 BIOMETRIC LOGIN +// // =================================================== +// router.post('/biometric/login', async (req, res) => { +// const data = await controller_login_post.onBiometricLogin(req, res) +// if (data) +// return sendResponse(res, 200, 'เข้าสู่ระบบผ่าน Biometric สำเร็จ', 'Biometric login succeed', data) +// }) + +// // =================================================== +// // 🔹 BIOMETRIC REGISTER (ต้อง login ก่อน) +// // =================================================== +// router.post('/biometric/register', authMiddleware, async (req, res) => { +// const data = await controller_login_post.onBiometricRegister(req, res) +// if (data) +// return sendResponse(res, 200, 'ผูก Biometric สำเร็จ', 'Biometric registered', data) +// }) + +// // =================================================== +// // 🔹 TOKEN RENEW (ต่ออายุ Token) +// // =================================================== +// router.post('/token/renew', authMiddleware, async (req, res) => { +// const data = await controller_login_post.onRenewToken(req, res) +// if (data) +// return sendResponse(res, 200, 'ออก Token ใหม่สำเร็จ', 'Token renewed', data) +// }) + +export default router diff --git a/exthernal-ttc-api/src/services/accountingSearchService.js b/exthernal-ttc-api/src/services/accountingSearchService.js new file mode 100644 index 0000000..bb0150a --- /dev/null +++ b/exthernal-ttc-api/src/services/accountingSearchService.js @@ -0,0 +1,27 @@ +import { GeneralService } from '../share/generalservice.js' + +export class AccountingSearchService { + + constructor() { + this.generalService = new GeneralService() + } + + async getAccountingSearch(database, id, username) { + const sql = ` + SELECT + actseq, + actnum, + acttyp, + ${database}.translatedtl('ACTTYP', acttyp) as acttypnam, + ${database}.translatedtl_multi(ARRAY['ACTCAT_INC', 'ACTCAT_EXP'], actcat) as actcatnam, + actqty, + actcmt, + actacpdtm + FROM ${database}.actmst + WHERE actnum = $1 + ` + const params = [id] + const result = await this.generalService.executeQueryParam(database, sql, params); + return result + } +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/services/accountingSetupService.js b/exthernal-ttc-api/src/services/accountingSetupService.js new file mode 100644 index 0000000..a3aba89 --- /dev/null +++ b/exthernal-ttc-api/src/services/accountingSetupService.js @@ -0,0 +1,27 @@ +import { GeneralService } from '../share/generalservice.js' + +export class AccountingSetupService { + constructor() { + this.generalService = new GeneralService() + } + async getAccountingSetup(database) { + const sql = ` + SELECT + dtlnam, + dtlcod, + dtltblcod + FROM ${database}.dtlmst + WHERE dtltblcod IN ('ACTTYP', 'ACTCAT_INC', 'ACTCAT_EXP'); + ` + const params = [] + const result = await this.generalService.executeQueryParam(database, sql, params); + return result + } +} + + + // SELECT + // acttyp, + // dtlname + // FROM ${database}.actmst + // LEFT JOIN ${database}.dtlmst ON dtltblcod = 'acttyp' and acttyp = dtlcod \ No newline at end of file diff --git a/exthernal-ttc-api/src/services/accountingSumService.js b/exthernal-ttc-api/src/services/accountingSumService.js new file mode 100644 index 0000000..caedd62 --- /dev/null +++ b/exthernal-ttc-api/src/services/accountingSumService.js @@ -0,0 +1,44 @@ +import { GeneralService } from '../share/generalservice.js' + +export class AccountingSumService { + constructor() { + this.generalService = new GeneralService() + } + async getAccountingSum(database, id) { + const sql = ` + SELECT + actseq, + actnum, + acttyp, + ${database}.translatedtl('ACTTYP', acttyp) as acttypnam, + ${database}.translatedtl_multi(ARRAY['ACTCAT_INC', 'ACTCAT_EXP'], actcat) as actcatnam, + actqty, + actcmt, + actacpdtm + FROM ${database}.actmst + WHERE actnum = $1 + ` + const params = [id] + const result = await this.generalService.executeQueryParam(database, sql, params); + return result + } + + + async getCategoryColorMap(database) { + const sql = ` + SELECT dtlcod, dtlnam, dtlmsc as dtlclr + FROM ${database}.dtlmst + WHERE dtltblcod IN ('ACTCAT_INC', 'ACTCAT_EXP') + `; + const params = [] + const rows = await this.generalService.executeQueryParam(database, sql, params); + + const map = {}; + rows.forEach(r => { + map[r.dtlnam] = r.dtlclr; // ใช้ชื่อหมวดเป็น key + }); + + return map; + } + +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/share/generalservice.js b/exthernal-ttc-api/src/share/generalservice.js new file mode 100644 index 0000000..c2cada3 --- /dev/null +++ b/exthernal-ttc-api/src/share/generalservice.js @@ -0,0 +1,177 @@ +import { connection } from '../config/db.js' +import dotenv from 'dotenv' +import { sendError } from '../utils/response.js' +dotenv.config() + +export class GeneralService { + devhint(level, location, message, extra = null) { + const isEnabled = process.env.DEVHINT === 'true' + const currentLevel = parseInt(process.env.DEVHINT_LEVEL || '1', 10) + if (!isEnabled || level > currentLevel) return + + const timestamp = new Date().toISOString() + const prefix = `🧩 [DEVHINT:${location}]` + const formatted = `${prefix} → ${message} (${timestamp})` + + // 🔹 highlight jumpout + if (message.includes('Jumpout')) { + console.log('\x1b[31m%s\x1b[0m', formatted) // แดง = jumpout + } else if (message.includes('Error')) { + console.log('\x1b[33m%s\x1b[0m', formatted) // เหลือง = error + } else { + console.log(formatted) + } + + if (extra) console.log(extra) + } + + // =================================================== + // ✅ executeQueryParam() — เจ๊งจริง แล้ว controller catch ได้จริง + // =================================================== + async executeQueryParam(database, sql, params = []) { + const formattedSQL = sql.replace(/\${database}/g, database) + try { + this.devhint(2, 'executeQueryParam', `📤 Executing Query`, `sql = ${formattedSQL}`) + const result = await connection.query(formattedSQL, params) + this.devhint(2, 'executeQueryParam', `✅ Query Success (${result.rowCount} rows)`) + return result.rows + } catch (err) { + this.devhint(1, 'executeQueryParam', `❌ SQL Error`, err.message) + console.error('🧨 SQL Error:', err.message) + throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊ง” แล้วโยนขึ้น controller จริง + } + } + + // =================================================== + // ✅ executeQueryConditions() — เหมือนกัน + // =================================================== + async executeQueryConditions(database, baseQuery, conditions = {}) { + this.devhint(2, 'GeneralService', 'executeQueryConditions() start') + + let whereClauses = [] + let params = [] + let idx = 1 + + for (const [key, value] of Object.entries(conditions)) { + if (value === undefined || value === null || value === '') continue + const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i) + if (match) { + const operator = match[1].toUpperCase() + const pattern = match[2].trim() + whereClauses.push(`${key} ${operator} $${idx}`) + params.push(pattern) + } else { + whereClauses.push(`${key} = $${idx}`) + params.push(value) + } + idx++ + } + + let finalQuery = baseQuery + if (whereClauses.length > 0) finalQuery += ' AND ' + whereClauses.join(' AND ') + const formattedSQL = finalQuery.replace(/\${database}/g, database) + + try { + this.devhint(2, 'executeQueryConditions', `📤 Executing Query`, { + database, + sql: formattedSQL, + params + }) + const result = await connection.query(formattedSQL, params) + this.devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`) + return result.rows + } catch (err) { + this.devhint(1, 'executeQueryConditions', `❌ SQL Error`, err.message) + console.error('🧨 SQL Error:', err.message) + throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊งจริง” ส่งถึง controller แน่นอน + } + } +} +// =================================================== +// Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย +// =================================================== + + + + +// /** +// * ✅ executeQueryParam (ของเดิม) +// * ใช้กับ SQL + database schema + params +// */ +// export async function executeQueryParam(sql, database, params = []) { +// try { +// if (!database) throw new Error('Database is not defined') + +// const formattedSQL = sql.replace(/\${database}/g, database) +// console.log(`[DB:${database}] → ${formattedSQL}`) +// const result = await connection.query(formattedSQL, params) +// return result.rows +// } catch (err) { +// console.error('[executeQueryParam Error]', err.message) +// throw err +// } +// } + +/** + * ✅ executeQueryConditions (ใหม่) + * ใช้สร้าง WHERE อัตโนมัติจาก object เงื่อนไข + * ตัวที่ไม่มีค่า (null, undefined, '') จะไม่ถูกนำมาสร้างใน WHERE + */ +// export async function executeQueryConditions(database, baseQuery, conditions = {}) { +// try { +// if (!database) throw new Error('Database is not defined') + +// let whereClauses = [] +// let params = [] +// let idx = 1 + +// for (const [key, value] of Object.entries(conditions)) { +// if (value === undefined || value === null || value === '') continue + +// // ✅ ตรวจว่า value มีคำว่า LIKE หรือ ILIKE ไหม +// const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i) +// if (match) { +// const operator = match[1].toUpperCase() +// const pattern = match[2].trim() +// whereClauses.push(`${key} ${operator} $${idx}`) +// params.push(pattern) +// } else { +// whereClauses.push(`${key} = $${idx}`) +// params.push(value) +// } + +// idx++ +// } + +// let finalQuery = baseQuery +// if (whereClauses.length > 0) { +// finalQuery += ' AND ' + whereClauses.join(' AND ') +// } + +// const formattedSQL = finalQuery.replace(/\${database}/g, database) +// console.log(`[DB:${database}] → ${formattedSQL}`) + +// const result = await connection.query(formattedSQL, params) +// return result.rows +// } catch (err) { +// console.error('[executeQueryConditions Error]', err.message) +// throw err +// } +// } + + +// /** +// * 🧩 devhint — Debug tracer ที่เปิดปิดได้จาก .env +// * @param {string} fileName - ชื่อไฟล์หรือโมดูล (เช่น 'usercontroller.js') +// * @param {string} message - ข้อความหรือจุดใน flow (เช่น 'onNavigate') +// * @param {object|string} [extra] - ข้อมูลเพิ่มเติม (optional) +// */ +// export function devhint(fileName, message, extra = null) { +// if (process.env.DEVHINT === 'true') { +// const timestamp = new Date().toISOString() +// const prefix = `🧩 [DEVHINT:${fileName}]` +// const formatted = `${prefix} → ${message} (${timestamp})` +// if (extra) console.log(formatted, '\n', extra) +// else console.log(formatted) +// } +// } diff --git a/exthernal-ttc-api/src/utils/errorList.js b/exthernal-ttc-api/src/utils/errorList.js new file mode 100644 index 0000000..63a756d --- /dev/null +++ b/exthernal-ttc-api/src/utils/errorList.js @@ -0,0 +1,40 @@ +// utils/errorList.js + +export function manualError(key) { + switch (key) { + case "invalid_input": + return { + code: 400, + messageTh: "ข้อมูลที่ส่งมาไม่ถูกต้อง", + messageEn: "Invalid input data" + }; + + case "not_found": + return { + code: 404, + messageTh: "ไม่พบข้อมูลที่ร้องขอ", + messageEn: "Resource not found" + }; + + case "unauthorized": + return { + code: 401, + messageTh: "คุณไม่มีสิทธิ์เข้าถึงข้อมูลนี้", + messageEn: "Unauthorized access" + }; + + case "server_error": + return { + code: 500, + messageTh: "เกิดข้อผิดพลาดภายในระบบ", + messageEn: "Internal server error" + }; + + default: + return { + code: 500, + messageTh: "ข้อผิดพลาดที่ไม่ทราบสาเหตุ", + messageEn: "Unknown error occurred" + }; + } +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/utils/mailer.js b/exthernal-ttc-api/src/utils/mailer.js new file mode 100644 index 0000000..0237e29 --- /dev/null +++ b/exthernal-ttc-api/src/utils/mailer.js @@ -0,0 +1,62 @@ +import nodemailer from 'nodemailer' +import dotenv from 'dotenv' +dotenv.config() + +export async function sendMockOtpMail(to, otp) { + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }) + +const html = ` +
+
+ Logo +

รหัส OTP สำหรับเปลี่ยนรหัสผ่าน

+

กรุณาใส่รหัสยืนยันด้านล่างนี้

+
${otp}
+

รหัสนี้จะหมดอายุใน 5 นาที

+
+

หากคุณไม่ได้ร้องขอการเปลี่ยนรหัสผ่าน กรุณาละเว้นอีเมลนี้

+
+
+` + + await transporter.sendMail({ + from: `"Support" <${process.env.SMTP_USER}>`, + to, + subject: 'OTP สำหรับเปลี่ยนรหัสผ่าน', + html + }) +} diff --git a/exthernal-ttc-api/src/utils/oftenError.js b/exthernal-ttc-api/src/utils/oftenError.js new file mode 100644 index 0000000..4f21d10 --- /dev/null +++ b/exthernal-ttc-api/src/utils/oftenError.js @@ -0,0 +1,41 @@ +// utils/oftenError.js +import { manualError } from "./errorList.js"; + +export class OftenError extends Error { + /** + * ใช้ได้ 2 แบบ: + * 1. throw new OftenError("not_found") + * 2. throw new OftenError(400, "ไทย", "English") + */ + constructor(arg1, arg2, arg3) { + // แบบ lookup จาก key + if (typeof arg1 === "string" && !arg2 && !arg3) { + const found = manualError(arg1); + super(found.messageEn); + this.statusCode = found.code; + this.messageTh = found.messageTh; + this.messageEn = found.messageEn; + this.key = arg1; + } + + // แบบ manual + else if (typeof arg1 === "number" && arg2 && arg3) { + super(arg3); + this.statusCode = arg1; + this.messageTh = arg2; + this.messageEn = arg3; + this.key = "manual"; + } + + // fallback + else { + super("Invalid error format"); + this.statusCode = 500; + this.messageTh = "รูปแบบการสร้าง error ไม่ถูกต้อง"; + this.messageEn = "Invalid error constructor format"; + this.key = "invalid_format"; + } + + this.name = "OftenError"; + } +} \ No newline at end of file diff --git a/exthernal-ttc-api/src/utils/otp.js b/exthernal-ttc-api/src/utils/otp.js new file mode 100644 index 0000000..78185b7 --- /dev/null +++ b/exthernal-ttc-api/src/utils/otp.js @@ -0,0 +1,3 @@ +export function generateOTP(length = 6) { + return Math.floor(100000 + Math.random() * 900000).toString() +} diff --git a/exthernal-ttc-api/src/utils/redis.js b/exthernal-ttc-api/src/utils/redis.js new file mode 100644 index 0000000..fd9023c --- /dev/null +++ b/exthernal-ttc-api/src/utils/redis.js @@ -0,0 +1,28 @@ +import Redis from 'ioredis' +import dotenv from 'dotenv' +dotenv.config() + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT +}) + +export async function saveOtp(email, otp) { + const key = `otp:${email}` + const ttl = parseInt(process.env.OTP_TTL_SECONDS || '300') + await redis.setex(key, ttl, otp) +} + +export async function verifyOtp(email, otp) { + const key = `otp:${email}` + const stored = await redis.get(key) + if (!stored) return false + return stored === otp +} + +export async function removeOtp(email) { + const key = `otp:${email}` + await redis.del(key) +} + +export default redis diff --git a/exthernal-ttc-api/src/utils/response.js b/exthernal-ttc-api/src/utils/response.js new file mode 100644 index 0000000..ed277ac --- /dev/null +++ b/exthernal-ttc-api/src/utils/response.js @@ -0,0 +1,24 @@ +// =================================================== +// ⚙️ Nuttakit Response Layer vFinal++++++ +// =================================================== + +export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400) { + return { + code: String(code), + message: enMsg, + message_th: thMsg, + data: [] + } +} + +// =================================================== +// 🔹 Auto Success Response (ใช้โดย Global Handler เท่านั้น) +// =================================================== +export function formatSuccessResponse(data) { + return { + code: "200", + message: "successful", + message_th: "ดำเนินการสำเร็จ", + data: data || null + } +} diff --git a/exthernal-ttc-api/src/utils/token.js b/exthernal-ttc-api/src/utils/token.js new file mode 100644 index 0000000..6fb2920 --- /dev/null +++ b/exthernal-ttc-api/src/utils/token.js @@ -0,0 +1,16 @@ +import jwt from 'jsonwebtoken' +import dotenv from 'dotenv' +dotenv.config() + +export function generateToken(payload) { + return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '24h' }) +} + +export function verifyToken(token) { + try { + return jwt.verify(token, process.env.JWT_SECRET) + } catch (err) { + console.error("❌ JWT verify error:", err.message); + return null + } +} diff --git a/exthernal-ttc-api/src/utils/trim.js b/exthernal-ttc-api/src/utils/trim.js new file mode 100644 index 0000000..5b9282e --- /dev/null +++ b/exthernal-ttc-api/src/utils/trim.js @@ -0,0 +1,11 @@ +export function trim_all_array(data) { + if (!Array.isArray(data)) return data + for (let row of data) { + for (let key in row) { + if (typeof row[key] === 'string') { + row[key] = row[key].trim() + } + } + } + return data +}