Files
wms-app/scripts/sdlc-seed/engineering.mjs
T

352 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Software requirements, software units, test cases and the traceability mapping.
import { CRS } from './project.mjs';
/** Software Requirements (SR01–SR09): [id, text, crRefs]. */
export const SR_GROUPS = [
['SR01', 'Standards of Software', [
['SR01:001', 'ระบบต้องพัฒนาและจัดทำเอกสารให้สอดคล้องกับมาตรฐาน ISO/IEC 29110 Basic Profile และแนวปฏิบัติ Secure Coding', 'CR12:004, CR14:001–CR14:003, CR09:003, CR10:004, CR11:001–CR11:004'],
['SR01:002', 'ระบบต้องใช้เทคโนโลยี PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js (Socket.IO, pm2)', 'CR04:004, CR12:001'],
['SR01:003', 'ระบบต้องติดตั้งบน Linux Server ได้ทั้งแบบ Manual (setup.php) และ Docker Compose (php-apache, mariadb, node/pm2)', 'CR07:001, CR10:001, CR10:002, CR08:001, CR08:002, CR13:003'],
['SR01:004', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt (PASSWORD_BCRYPT) และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา', 'CR12:003'],
['SR01:005', 'ค่าตั้งค่าและความลับ (app/config.php, .env) ต้องถูกยกเว้นจาก Source Control และสร้างขึ้นตอนติดตั้ง', 'CR06:002, CR09:002, CR10:003'],
['SR01:006', 'แอปพลิเคชันและงานตามกำหนดเวลาต้องใช้เขตเวลา Asia/Bangkok', 'CR07:002'],
]],
['SR02', 'Software Structure Considerations', [
['SR02:001', 'ระบบต้องเป็น Web-based Application แบบหลายหน้า (Multi-page PHP)', 'CR04:001'],
['SR02:002', 'ส่วนติดต่อผู้ใช้ต้องเป็น Responsive Design รองรับ Desktop / Tablet / Mobile', 'CR05:001, CR07:003'],
['SR02:003', 'ตรรกะทางธุรกิจต้องจัดเป็น Manager/Service Classes ที่เรียกใช้จาก Page Controller แยกจากส่วนแสดงผล', 'CR09:001'],
['SR02:004', 'การเข้าถึงข้อมูลต้องผ่าน Manager Classes ไม่กระจาย SQL ในหน้าเพจ', 'CR09:001'],
['SR02:005', 'ระบบต้องติดตั้งเป็น Container Stack ผ่าน Docker Compose ได้ นอกเหนือจากการติดตั้งแบบ Manual', 'CR10:002'],
]],
['SR03', 'Software Elements', [
['SR03:001', 'โมดูลผู้ใช้งานและสิทธิ์: ลงทะเบียน, Onboarding, เข้าสู่ระบบ, บทบาท Owner/Admin/Staff/Viewer, สิทธิ์แอปพลิเคชัน (app/login/, UserManager, PasswordManager, PasswordResetManager)', 'CR01:001–CR01:004'],
['SR03:002', 'โมดูลตั้งค่าบริษัท: ข้อมูลบริษัท, SMTP, การตั้งค่าระบบ (app/setting/, CompanyProfileManager, CompanySettingManager, SmtpManager)', 'CR01:004, CR03:005'],
['SR03:003', 'โมดูลข้อมูลหลัก: คลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ผู้ติดต่อ (app/inventory/, app/contact/, WarehouseManager, ProductManager, ContactManager)', 'CR01:005, CR01:006'],
['SR03:004', 'โมดูลควบคุมสินค้าคงคลัง: รับเข้า, จ่ายออก, โอนย้าย, Lot/Serial/Expiry, บาร์โค้ด, แนบไฟล์ (app/ics/, StockManager, StockSourceManager, BarcodeManager, FileUploader)', 'CR01:007–CR01:012, CR01:019, CR05:005'],
['SR03:005', 'โมดูลขาย: ใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน/ใบลดหนี้ (app/order/, app/revenue/, QuotationManager, OrderManager, InvoiceManager, ReturnManager)', 'CR01:013'],
['SR03:006', 'โมดูลจัดซื้อ: ใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ, ใบคืนสินค้าผู้ขาย (app/po/, PurchaseRequestManager, PurchaseOrderManager, SupplierReturnManager)', 'CR01:014'],
['SR03:007', 'โมดูลการเงิน: ใบวางบิลรับ, ใบเสร็จ, ใบวางบิลจ่าย, ใบสำคัญจ่าย (app/finance/, ReceiptBillingManager, ReceiptManager, PaymentBillingManager, PaymentManager)', 'CR01:015'],
['SR03:008', 'โมดูลบัญชี: ผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน, บัญชีแยกประเภท (app/accounting/, app/journal/, PostingManager)', 'CR01:016'],
['SR03:009', 'โมดูลรายงานและแดชบอร์ด: รายงานสต๊อก, รายงานการเงิน, แดชบอร์ด (app/reports/, app/dashboard/, app/ac_dashboard/, ReportManager, EtlStockManager)', 'CR01:011, CR01:017, CR01:020'],
['SR03:010', 'โมดูลควบคุมเอกสาร: ออกเลขที่เอกสาร, วงจรชีวิต/สถานะ, ประวัติรายการ (DocumentNumberManager, BatchActionManager)', 'CR01:018, CR01:023'],
['SR03:011', 'บริการแจ้งเตือนและงานตามกำหนดเวลา: Socket.IO และ Scheduler (nodejs/server.js, nodejs/scheduler.js, app/cron/)', 'CR01:021, CR01:022'],
]],
['SR04', 'Software Elements Relationship', [
['SR04:001', 'ทุกโมดูลปฏิบัติการต้องเข้าถึงได้ผ่านการยืนยันตัวตนและการตรวจสิทธิ์ของโมดูลผู้ใช้งานเท่านั้น', 'CR01:002, CR01:024, CR04:003, CR05:002'],
['SR04:002', 'โมดูลขาย จัดซื้อ และการเงิน ต้องบันทึกผลกระทบต่อสต๊อกและ GL ผ่านโมดูลคลังสินค้าและบัญชี ไม่ทำซ้ำตรรกะ', 'CR01:013–CR01:016'],
['SR04:003', 'โมดูลรายงานต้องอ่านข้อมูลภายใต้ขอบเขตบริษัท/คลังที่โมดูลข้อมูลหลักและคลังสินค้าบังคับใช้', 'CR01:024, CR08:004'],
['SR04:004', 'บริการ Node.js ต้องรับเหตุการณ์จากแอปพลิเคชัน PHP ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret เท่านั้น', 'CR01:021, CR03:003'],
['SR04:005', 'ทุกโมดูลที่สร้างเอกสารธุรกิจต้องเรียกใช้โมดูลควบคุมเอกสารเพื่อออกเลขที่เอกสาร', 'CR01:018'],
]],
['SR05', 'Performance Characteristics', [
['SR05:001', 'งานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ต้องเสร็จภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง', 'CR02:001'],
['SR05:002', 'ยอดสรุปสต๊อก/GL/แดชบอร์ดต้องคำนวณล่วงหน้าตามกำหนดเวลา (etl_stock_summary, etl_gl_summary)', 'CR02:002, CR01:022, CR08:005'],
['SR05:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันตามที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง', 'CR02:003'],
['SR05:004', 'ระบบต้องมี Health Check ทุก 5 นาที และแจ้งเตือนผู้ดูแลเมื่อล้มเหลวติดต่อกัน 2 ครั้ง', 'CR08:003'],
]],
['SR06', 'Software Interfaces', [
['SR06:001', 'แอปพลิเคชัน PHP ต้องเชื่อมต่อฐานข้อมูล MariaDB 2 ฐาน: wms (ผู้ใช้/บริษัท) และ wms2 (คลัง/บัญชี)', 'CR03:001'],
['SR06:002', 'ระบบต้องใช้งานได้บน Chrome, Edge และ Firefox รุ่นปัจจุบัน', 'CR03:002'],
['SR06:003', 'ระบบต้องรองรับ Mobile/Tablet Browser ผ่าน Responsive UI', 'CR07:003'],
['SR06:004', 'แอปพลิเคชัน PHP ต้องส่งเหตุการณ์ไป Node.js ผ่าน HTTP Endpoint ภายใน (NODE_EMIT_URL + NODE_EMIT_SECRET) และส่งอีเมลผ่าน SMTP ต่อบริษัท', 'CR03:003, CR03:005'],
['SR06:005', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint (NODE_PUBLIC_URL) เพื่อรับการแจ้งเตือน', 'CR03:004'],
]],
['SR07', 'Security Characteristics', [
['SR07:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องผ่าน HTTPS/TLS', 'CR06:001'],
['SR07:002', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง', 'CR06:003, CR14:004'],
['SR07:003', 'ระบบต้องอนุญาต Session ที่ใช้งานอยู่เพียงหนึ่งต่อบัญชี (บล็อก Concurrent Login)', 'CR06:006'],
['SR07:004', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting ทั้งด้านรับข้อมูลและแสดงผล', 'CR06:005'],
['SR07:005', 'RBAC (Owner/Admin/Staff/Viewer) ต้องบังคับใช้ที่ฝั่งเซิร์ฟเวอร์ ไม่ใช่เพียงการซ่อนเมนู', 'CR06:004'],
]],
['SR08', 'Database Design Requirements', [
['SR08:001', 'ฐานข้อมูล wms: user, company_list, company_map_user, company_setting, company_smtp, company_usage, whitelist', 'CR01:001–CR01:004, CR04:002'],
['SR08:002', 'ตารางข้อมูลหลัก (wms2): md_warehouse, md_storage, md_bin, md_product, md_product_category, md_contact, md_contact_type, md_account, md_account_formula, md_account_formula_item, md_department, md_barcode, md_sku_barcode_label, md_lot, md_lock_operation', 'CR01:005, CR01:010, CR01:016'],
['SR08:003', 'ตารางรายการ (td_*): td_stock, td_order/_item, td_quotation/_item, td_invoice/_item, td_return/_item, td_purchase_request/_item, td_purchase_order/_item, td_supplier_return/_item, td_receipt/_item, td_receipt_billing/_item, td_payment/_item, td_payment_billing/_item, td_gl/td_gl_item, td_batch_action, td_bin_log', 'CR01:007–CR01:018'],
['SR08:004', 'ตารางสรุปยอดและควบคุมเอกสาร: etl_stock_summary, etl_gl_summary, document_number_sequences, document_types, schema_migrations', 'CR01:018, CR01:022, CR09:004'],
['SR08:005', 'ตารางทั้งหมดต้องสร้างซ้ำได้ในสภาพแวดล้อมใหม่ผ่าน setup.php โดยไม่ต้องแก้ Schema ด้วยมือ', 'CR10:001, CR09:004'],
]],
['SR09', 'Error Handling and Recovery Attributes', [
['SR09:001', 'การลบ ยกเลิก หรือ Void เอกสารควบคุมต้องมี Confirm Dialog', 'CR05:004, CR01:023'],
['SR09:002', 'ข้อผิดพลาดต้องแสดงข้อความที่ผู้ใช้เข้าใจได้แทน Fatal Error', 'CR05:003, CR13:004'],
['SR09:003', 'การเปลี่ยนแปลงที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction และป้องกันยอดติดลบ/ซ้ำ', 'CR13:001, CR13:002, CR01:008'],
]],
];
export const SRS = SR_GROUPS.flatMap(([, , rows]) => rows);
export const srTopic = (id) => SRS.find((r) => r[0] === id)?.[1] ?? '';
/** Software Units grouped by module: [moduleId, moduleName, [[unitId, description, functional interface detail, srRefs, files]]]. */
export const UNIT_GROUPS = [
['UN01', 'ผู้ใช้งานและสิทธิ์ (Identity & Access)', [
['UN01.001', 'Identity – Register & Onboarding', 'ลงทะเบียนเจ้าของบริษัท, ยืนยันอีเมล, เชิญผู้ใช้และรับคำเชิญเข้าบริษัทที่ถูกต้อง', 'SR03:001, SR08:001', 'app/login/, UserManager.php'],
['UN01.002', 'Identity – Login & Role Guard', 'เข้าสู่ระบบด้วย Username/Password, ตรวจบทบาทและสิทธิ์แอปพลิเคชันฝั่งเซิร์ฟเวอร์ก่อนเข้าทุกหน้า', 'SR03:001, SR04:001, SR07:005', 'app/login/, UserManager.php, app/assets/utils/'],
['UN01.003', 'Identity – Password Recovery / OTP / Session', 'ลืมรหัสผ่านผ่านอีเมล, OTP, Hash รหัสผ่าน bcrypt, บล็อก Concurrent Login', 'SR03:001, SR01:004, SR07:003', 'PasswordManager.php, PasswordResetManager.php'],
['UN01.004', 'Identity – User & App-Access Administration', 'Owner เชิญ/แก้ไขบทบาทและสิทธิ์แอปพลิเคชัน/ลบผู้ใช้ และส่งคำเชิญซ้ำ', 'SR03:001, SR08:001', 'app/setting/api/engine/manage_users.php, UserManager.php'],
]],
['UN02', 'ตั้งค่าบริษัท (Company Settings)', [
['UN02.001', 'Company – Profile & System Settings', 'แก้ไขข้อมูลบริษัท, โลโก้, ค่าตั้งค่าระบบต่อบริษัท', 'SR03:002', 'CompanyProfileManager.php, CompanySettingManager.php'],
['UN02.002', 'Company – SMTP & Mail Dispatch', 'ตั้งค่า SMTP ต่อบริษัท และส่งอีเมล Onboarding/กู้คืนรหัสผ่าน/แจ้งเตือน', 'SR03:002, SR06:004', 'SmtpManager.php'],
]],
['UN03', 'ข้อมูลหลัก (Master Data)', [
['UN03.001', 'Master – Warehouse / Storage / Bin', 'จัดการคลังสินค้า พื้นที่ และช่องจัดเก็บ พร้อมความจุ', 'SR03:003, SR08:002', 'app/inventory/, WarehouseManager.php'],
['UN03.002', 'Master – Product & Category', 'จัดการหมวดสินค้า สินค้า หน่วยนับ และรูปสินค้า', 'SR03:003, SR08:002', 'ProductManager.php'],
['UN03.003', 'Master – Contact', 'จัดการประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'SR03:003, SR08:002', 'app/contact/, ContactManager.php'],
['UN03.004', 'Master – Warehouse Layer Configuration', 'สลับโครงสร้างคลังแบบชั้นเดียว/หลายชั้นต่อบริษัท', 'SR03:003', 'CompanySettingManager.php, WarehouseManager.php'],
]],
['UN04', 'ควบคุมสินค้าคงคลัง (Inventory Control)', [
['UN04.001', 'ICS – Stock-in', 'รับสินค้าเข้าตามสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง สร้างความเคลื่อนไหวและยอดคงเหลือ', 'SR03:004, SR08:003', 'app/ics/, StockManager.php, StockTablesTrait.php'],
['UN04.002', 'ICS – Stock-out', 'จ่ายสินค้าออกทั้งยอดของช่องจัดเก็บโดยตรวจสิทธิ์ ยอดคงเหลือไม่ติดลบ', 'SR03:004, SR09:003', 'StockManager.php, OrderManager.php'],
['UN04.003', 'ICS – Stock Transfer', 'โอนย้ายระหว่างตำแหน่ง ต้นทาง/ปลายทางผูกเป็นรายการเดียว', 'SR03:004', 'StockManager.php'],
['UN04.004', 'ICS – Lot / Serial / Expiry', 'บันทึกและสอบกลับ Lot, Serial Number, วันหมดอายุ', 'SR03:004, SR08:002', 'StockSourceManager.php, md_lot'],
['UN04.005', 'ICS – Barcode Label & Scan', 'พิมพ์ฉลากบาร์โค้ด SKU/ตำแหน่ง และรับค่าจากเครื่องสแกน', 'SR03:004', 'BarcodeManager.php, location_barcode_label.php'],
['UN04.006', 'ICS – File Attachment', 'แนบและเรียกดูไฟล์ที่อนุญาตในรายการที่รองรับ', 'SR03:004', 'FileUploader.php'],
]],
['UN05', 'ขาย (Sales)', [
['UN05.001', 'Sales – Quotation', 'สร้าง/แก้ไข/อนุมัติใบเสนอราคา และแปลงเป็นใบสั่งขาย', 'SR03:005, SR04:002', 'app/order/, QuotationManager.php'],
['UN05.002', 'Sales – Sales Order', 'สร้างใบสั่งขาย ยืนยัน และตัดสต๊อกตามสถานะ', 'SR03:005, SR04:002', 'OrderManager.php'],
['UN05.003', 'Sales – Invoice', 'ออกใบแจ้งหนี้จากใบสั่งขาย และบันทึก GL', 'SR03:005, SR04:002', 'app/revenue/, InvoiceManager.php'],
['UN05.004', 'Sales – Return / Credit Note', 'รับคืนสินค้าและออกใบลดหนี้ พร้อมปรับสต๊อก/บัญชี', 'SR03:005, SR04:002', 'ReturnManager.php'],
]],
['UN06', 'จัดซื้อ (Purchasing)', [
['UN06.001', 'Purchasing – Purchase Request', 'สร้างและอนุมัติใบขอซื้อ', 'SR03:006', 'app/po/, PurchaseRequestManager.php'],
['UN06.002', 'Purchasing – Purchase Order', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ และติดตามสถานะ', 'SR03:006, SR04:002', 'PurchaseOrderManager.php'],
['UN06.003', 'Purchasing – Purchase Invoice', 'รับสินค้าตามใบสั่งซื้อ และบันทึกใบแจ้งหนี้ซื้อพร้อมผ่าน GL', 'SR03:006, SR04:002', 'PurchaseOrderManager.php, app/expense/purchase_invoice.php, PurchaseInvoicePosting.php'],
['UN06.004', 'Purchasing – Supplier Return', 'คืนสินค้าผู้ขาย พร้อมปรับสต๊อก/บัญชี', 'SR03:006, SR04:002', 'SupplierReturnManager.php'],
]],
['UN07', 'การเงิน (Finance)', [
['UN07.001', 'Finance – Receipt Billing & Receipt', 'วางบิลรับและบันทึกใบเสรจรับเงินผูกกับใบแจ้งหนี้', 'SR03:007, SR04:002', 'app/finance/, ReceiptBillingManager.php, ReceiptManager.php'],
['UN07.002', 'Finance – Payment Billing & Payment', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายผูกกับใบแจ้งหนี้ซื้อ', 'SR03:007, SR04:002', 'PaymentBillingManager.php, PaymentManager.php'],
]],
['UN08', 'บัญชี (Accounting)', [
['UN08.001', 'Accounting – Chart of Accounts / Departments / Formulas', 'จัดการผังบัญชี แผนก และสูตรบัญชีอัตโนมัติ', 'SR03:008, SR08:002', 'app/accounting/, GlManager.php'],
['UN08.002', 'Accounting – Journal & GL Posting', 'บันทึกสมุดรายวันและผ่านรายการ GL ตามช่วงเวลาที่เปิดผ่านรายการ', 'SR03:008, SR09:003', 'app/accounting/api/engine/save_manual_journal.php, GlManager.php, BasePosting.php, PostingWindowGuard.php'],
]],
['UN09', 'รายงานและแดชบอร์ด (Reports & Dashboard)', [
['UN09.001', 'Reports – Dashboard & Aggregates', 'แดชบอร์ดคลังสินค้าและบัญชีจากตารางสรุปยอด', 'SR03:009, SR05:001, SR05:002', 'app/dashboard/, app/ac_dashboard/, EtlStockManager.php'],
['UN09.002', 'Reports – Stock Reports', 'ภาพรวมสต๊อก, ความเคลื่อนไหว, ความจุ, สินค้าใกล้หมด, หมดอายุ, Lot', 'SR03:009', 'app/reports/, ReportManager.php'],
['UN09.003', 'Reports – Financial Reports', 'งบทดลอง, งบกำไรขาดทุน, งบดุล, VAT, สมุดรายวัน, ความเคลื่อนไหว GL', 'SR03:009', 'app/accounting/ (trial_balance, pl_statement, balance_sheet, vat_report, journal_listing, gl_movement)'],
['UN09.004', 'Reports – Filter / Print / Export', 'กรอง ดู พิมพ์ และส่งออกรายงานตามขอบเขตสิทธิ์', 'SR03:009, SR04:003', 'app/reports/'],
]],
['UN10', 'ควบคุมเอกสาร (Document Control)', [
['UN10.001', 'Document – Numbering', 'ออกเลขที่เอกสารตามลำดับที่กำหนด (document_number_sequences)', 'SR03:010, SR04:005, SR08:004', 'DocumentNumberManager.php'],
['UN10.002', 'Document – Lifecycle & Status', 'ควบคุมสถานะเอกสาร ปฏิเสธการเปลี่ยนสถานะที่ไม่ถูกต้อง พร้อม Confirm Dialog และข้อความข้อผิดพลาด', 'SR03:010, SR09:001, SR09:002', 'BatchActionManager.php, Manager classes'],
['UN10.003', 'Document – Audit Fields & History', 'บันทึกผู้ทำรายการ วันเวลา และข้อมูลที่ส่งในคอลัมน์ log ของรายการ และประวัติช่องจัดเก็บ', 'SR03:010, SR09:001', 'db_auth.php (log), td_* tables, td_bin_log'],
]],
['UN11', 'แจ้งเตือนและงานตามกำหนดเวลา (Notification & Scheduler)', [
['UN11.001', 'Node – Socket.IO Notification Server', 'รับเหตุการณ์จาก PHP ผ่าน Endpoint ที่ป้องกันด้วย Secret และส่งต่อไปยัง Browser ของผู้ใช้ที่เกี่ยวข้อง', 'SR03:011, SR04:004, SR06:004, SR06:005', 'nodejs/server.js'],
['UN11.002', 'Node – Scheduler (ETL & Alerts)', 'สรุปยอดสต๊อก/GL, แจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระ ตามกำหนดเวลา Asia/Bangkok', 'SR03:011, SR05:002, SR01:006', 'nodejs/scheduler.js, app/cron/'],
]],
['UN12', 'ความปลอดภัยและการแยกข้อมูล (Security & Tenant Scope)', [
['UN12.001', 'Security – Tenant Scope Guard', 'จำกัดทุก Query และ Action ให้อยู่ในบริษัท/คลังที่ผู้ใช้ได้รับอนุญาต', 'SR04:001, SR04:003, SR07:002', 'StockTablesTrait.php, Manager classes'],
['UN12.002', 'Security – Server-side Validation', 'ตรวจสอบข้อมูลนำเข้า, Prepared Statements, Escape Output ป้องกัน SQLi/XSS', 'SR07:002, SR07:004', 'Manager classes, app/assets/utils/'],
['UN12.003', 'Security – Posting Window & Usage Guard', 'ปฏิเสธการผ่านรายการนอกช่วงเวลาที่เปิด และล็อกแดชบอร์ด/รายงานเมื่อใช้งานเกินแพ็กเกจ', 'SR07:002, SR09:003', 'PostingWindowGuard.php, UsageGuard.php'],
['UN12.004', 'Security – TLS & Secret Configuration', 'ค่าตั้งค่า TLS, NODE_EMIT_SECRET และ Secrets ที่สร้างตอนติดตั้ง', 'SR07:001, SR01:005', 'docker/php/config.php.template, .env'],
]],
['UN13', 'ติดตั้งและสำรองข้อมูล (Deployment & Backup)', [
['UN13.001', 'Deploy – setup.php Schema Installer', 'สร้างฐานข้อมูล wms/wms2 และตารางทั้งหมดในขั้นตอนเดียว', 'SR01:003, SR08:005, SR06:001', 'setup.php, docker/mariadb/init-wms2.sql'],
['UN13.002', 'Deploy – Docker Compose Stack', 'Service db, php, node พร้อม Entrypoint สร้าง config.php', 'SR02:005, SR01:003', 'docker-compose.yml, docker/'],
['UN13.003', 'Deploy – .env Generator', 'สร้าง .env และความลับอัตโนมัติ ไม่เก็บใน Git', 'SR01:005', 'docker/init-env.sh, .env.example'],
['UN13.004', 'Deploy – Backup & Restore', 'สำรอง Git 2 Remote, สำรองฐานข้อมูลด้วย mysqldump และขั้นตอนกู้คืน', 'SR01:003', 'Product Operation Guide, backup remote'],
]],
];
export const UNITS = UNIT_GROUPS.flatMap(([, , rows]) => rows);
export const unitTopic = (id) => UNITS.find((u) => u[0] === id)?.[1] ?? '';
export const unitModule = (id) => UNIT_GROUPS.find(([m]) => id.startsWith(m))?.[1] ?? '';
/**
* Test cases: one per software unit. [tcId, unitId, input, output, environment, special, dependency]
*/
const ENV_WEB = 'Web Browser (Chrome/Edge/Firefox), PHP 8, MariaDB, Internal Testing Server';
const ENV_NODE = 'Node.js + pm2, Internal Testing Server';
export const TEST_CASES = [
['TC-UN01.001', 'UN01.001', 'ลงทะเบียนเจ้าของบริษัทใหม่ → ยืนยันอีเมล → เชิญผู้ใช้ → ผู้ใช้กดลิงก์เชิญ', 'สร้างบริษัท/บัญชีเจ้าของสำเร็จ ผู้ใช้ที่ถูกเชิญเข้าบริษัทที่ถูกต้อง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องส่งอีเมลได้จริง', '-'],
['TC-UN01.002', 'UN01.002', 'เข้าสู่ระบบด้วยบัญชี Owner/Admin/Staff/Viewer แล้วเรียกหน้าและ Action ที่ไม่ได้รับสิทธิ์', 'แต่ละบทบาทเข้าถึงได้เฉพาะหน้าจอ/คำสั่งที่อนุญาต คำสั่งที่ไม่ได้รับสิทธิ์ถูกปฏิเสธฝั่งเซิร์ฟเวอร์', ENV_WEB, 'ต้องมีบัญชีทดสอบครบ 4 บทบาท', 'TC-UN01.001'],
['TC-UN01.003', 'UN01.003', 'ขอรีเซ็ตรหัสผ่าน → กรอก OTP → ตั้งรหัสใหม่; เข้าสู่ระบบบัญชีเดิมจาก 2 อุปกรณ์', 'รีเซ็ตสำเร็จโดยไม่เปิดเผยรหัสผ่านเดิม Session ที่สองถูกปฏิเสธ', ENV_WEB + ', SMTP ทดสอบ', '-', 'TC-UN01.002'],
['TC-UN01.004', 'UN01.004', 'Owner เชิญผู้ใช้ แก้ไขบทบาทและสิทธิ์แอปพลิเคชัน และลบผู้ใช้; Admin และ Staff ลองทำรายการเดียวกัน', 'การเปลี่ยนแปลงของ Owner ถูกบันทึก สิทธิ์แอปพลิเคชันไม่เกินสิทธิ์ของ Owner Admin และ Staff ถูกปฏิเสธ (HTTP 403)', ENV_WEB, 'บัญชี Owner, Admin, Staff ในบริษัทเดียวกัน', 'TC-UN01.002'],
['TC-UN02.001', 'UN02.001', 'แก้ไขข้อมูลบริษัทและค่าตั้งค่าระบบ', 'ค่าใหม่ถูกบันทึกและมีผลกับบริษัทนั้นเท่านั้น', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN02.002', 'UN02.002', 'ตั้งค่า SMTP แล้วส่งอีเมลทดสอบ/คำเชิญ', 'อีเมลถูกส่งผ่าน SMTP ของบริษัทและได้รับที่ปลายทาง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องมีบัญชี SMTP ทดสอบ', 'TC-UN02.001'],
['TC-UN03.001', 'UN03.001', 'เพิ่ม/แก้ไข/ปิดใช้ คลังสินค้า พื้นที่ และช่องจัดเก็บ; กรอกความจุไม่ถูกต้อง', 'รายการถูกต้องถูกบันทึก ข้อมูลผิดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.002', 'UN03.002', 'เพิ่มหมวดสินค้าและสินค้าพร้อมหน่วยนับ', 'สินค้าแสดงในรายการและเลือกใช้ในรายการสต๊อกได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.003', 'UN03.003', 'เพิ่มประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'ผู้ติดต่อเลือกใช้ในเอกสารขาย/ซื้อได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.004', 'UN03.004', 'ตั้งบริษัท A เป็นคลังชั้นเดียว บริษัท B เป็นคลัง/พื้นที่/ช่อง แล้วรับสินค้าเข้า', 'ทั้งสองบริษัททำรายการได้ถูกต้องตามโครงสร้างของตน', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN03.001'],
['TC-UN04.001', 'UN04.001', 'รับสินค้าเข้า ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง; กรอกจำนวนติดลบ', 'สร้างความเคลื่อนไหวและยอดคงเหลือถูกต้อง จำนวนติดลบถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN04.002', 'UN04.002', 'จ่ายสินค้าออกจากช่องจัดเก็บที่มียอด; ยืนยันใบสั่งขายที่จำนวนเกินยอดคงเหลือ', 'ยอดช่องจัดเก็บลดเป็นศูนย์และบันทึกความเคลื่อนไหว ยอดไม่ติดลบ ใบสั่งขายที่เกินยอดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.003', 'UN04.003', 'โอนย้ายระหว่าง 2 ตำแหน่งที่ได้รับอนุญาต', 'ต้นทางลด ปลายทางเพิ่ม ผูกเป็นรายการโอนเดียวและสอบกลับได้', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.004', 'UN04.004', 'รับสินค้าเข้าพร้อม Lot, Serial Number, วันหมดอายุ แล้วเปิดรายงาน Lot/หมดอายุ', 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.005', 'UN04.005', 'พิมพ์ฉลาก SKU/ตำแหน่ง แล้วสแกน (หรือป้อนค่า) ในหน้าจอรับเข้า', 'ฉลากมีรหัสที่ใช้งานได้ ค่าที่สแกนถูกยอมรับ', ENV_WEB + ', เครื่องสแกนหรือจำลอง', 'เครื่องพิมพ์/สแกน (ถ้ามี)', 'TC-UN03.002'],
['TC-UN04.006', 'UN04.006', 'แนบไฟล์ที่อนุญาตและไฟล์ที่ไม่อนุญาตกับรายการ; ผู้ใช้บริษัทอื่นเรียกดู', 'ไฟล์ที่อนุญาตอัปโหลด/เรียกดูได้เฉพาะผู้มีสิทธิ์ ไฟล์ที่ไม่อนุญาตถูกปฏิเสธ', ENV_WEB, 'เตรียมไฟล์ตัวอย่าง', 'TC-UN03.002'],
['TC-UN05.001', 'UN05.001', 'สร้างใบเสนอราคาให้ลูกค้าและแปลงเป็นใบสั่งขาย', 'ใบสั่งขายถูกสร้างพร้อมอ้างอิงใบเสนอราคา', ENV_WEB, '-', 'TC-UN03.003'],
['TC-UN05.002', 'UN05.002', 'ยืนยันใบสั่งขายและตรวจสต๊อก', 'สถานะเปลี่ยนตามที่อนุญาตและตัดสต๊อกถูกต้อง', ENV_WEB, '-', 'TC-UN05.001, TC-UN04.001'],
['TC-UN05.003', 'UN05.003', 'ออกใบแจ้งหนี้จากใบสั่งขาย', 'ใบแจ้งหนี้ได้เลขที่เอกสารและบันทึก GL สมดุล', ENV_WEB, '-', 'TC-UN05.002'],
['TC-UN05.004', 'UN05.004', 'รับคืนสินค้าและออกใบลดหนี้', 'สต๊อกเพิ่มกลับและบัญชีปรับตามใบลดหนี้', ENV_WEB, '-', 'TC-UN05.003'],
['TC-UN06.001', 'UN06.001', 'สร้างและอนุมัติใบขอซื้อ', 'ใบขอซื้ออยู่ในสถานะอนุมัติ', ENV_WEB, '-', 'TC-UN03.003'],
['TC-UN06.002', 'UN06.002', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ', 'ใบสั่งซื้อถูกสร้างพร้อมอ้างอิงใบขอซื้อ', ENV_WEB, '-', 'TC-UN06.001'],
['TC-UN06.003', 'UN06.003', 'บันทึกใบแจ้งหนี้ซื้อและรับสินค้าเข้าคลัง', 'สต๊อกเพิ่มและ GL บันทึกสมดุล', ENV_WEB, '-', 'TC-UN06.002'],
['TC-UN06.004', 'UN06.004', 'คืนสินค้าให้ผู้ขาย', 'สต๊อกลดและบัญชีปรับตามใบคืน', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN07.001', 'UN07.001', 'วางบิลรับและบันทึกใบเสร็จรับเงินกับใบแจ้งหนี้', 'ยอดค้างชำระลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN05.003'],
['TC-UN07.002', 'UN07.002', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายกับใบแจ้งหนี้ซื้อ', 'ยอดค้างจ่ายลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN08.001', 'UN08.001', 'เพิ่มผังบัญชี แผนก และสูตรบัญชี', 'โครงสร้างบัญชีถูกบันทึกและใช้กับการผ่านรายการได้', ENV_WEB, '-', 'TC-UN01.004'],
['TC-UN08.002', 'UN08.002', 'บันทึกสมุดรายวันแบบสมดุลและแบบไม่สมดุล; จำลองความล้มเหลวระหว่างผ่านรายการ', 'รายการสมดุลถูกผ่าน GL รายการไม่สมดุลถูกปฏิเสธ ความล้มเหลวถูก Rollback ทั้งชุด', ENV_WEB, 'ต้องจำลองความล้มเหลวบน Testing Server', 'TC-UN08.001'],
['TC-UN09.001', 'UN09.001', 'รันงานสรุปยอดแล้วเปิดแดชบอร์ดคลังสินค้าและบัญชี', 'ตัวเลขตรงกับตาราง etl_stock_summary และ etl_gl_summary และหน้าแดชบอร์ดแสดงผลภายใน 3 วินาที', ENV_WEB, '-', 'TC-UN11.002'],
['TC-UN09.002', 'UN09.002', 'เปิดรายงานภาพรวมสต๊อก ความเคลื่อนไหว ความจุ สินค้าใกล้หมด หมดอายุ และ Lot พร้อมตัวกรอง', 'รายงานแสดงข้อมูลที่ได้รับสิทธิ์ตามตัวกรอง', ENV_WEB, '-', 'TC-UN04.001–TC-UN04.004'],
['TC-UN09.003', 'UN09.003', 'เปิดงบทดลอง งบกำไรขาดทุน งบดุล VAT สมุดรายวัน และความเคลื่อนไหว GL ของงวดเดียวกัน', 'งบทดลองเดบิตเท่ากับเครดิต สินทรัพย์เท่ากับหนี้สินรวมส่วนของเจ้าของ และยอดในรายงานตรงกับสมุดรายวัน', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN09.004', 'UN09.004', 'กรอง ดู พิมพ์ และส่งออกรายงาน', 'ผลลัพธ์ตรงกับขอบเขตและตัวกรองที่เลือก', ENV_WEB, '-', 'TC-UN09.002'],
['TC-UN10.001', 'UN10.001', 'สร้างเอกสารควบคุมหลายฉบับติดต่อกัน', 'เลขที่เอกสารเรียงตามลำดับที่ตั้งค่า ไม่ซ้ำ', ENV_WEB, '-', 'TC-UN05.001'],
['TC-UN10.002', 'UN10.002', 'พยายามเปลี่ยนสถานะเอกสารข้ามขั้น; ลบเอกสารควบคุม', 'การเปลี่ยนสถานะที่ไม่ถูกต้องถูกปฏิเสธพร้อมข้อความ การลบต้องยืนยันก่อน', ENV_WEB, '-', 'TC-UN10.001'],
['TC-UN10.003', 'UN10.003', 'สร้างแล้วแก้ไขรายการ แล้วเปิดดูประวัติรายการ', 'คอลัมน์ log บันทึกผู้ทำรายการ วันเวลา และข้อมูลที่ส่งครบทุกครั้ง', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN11.001', 'UN11.001', 'เปลี่ยนสถานะเอกสารที่ต้องแจ้งเตือน; เรียก Endpoint แจ้งเตือนโดยไม่มี Secret', 'ผู้ใช้ที่เกี่ยวข้องได้รับแจ้งเตือน Real-time เท่านั้น การเรียกโดยไม่มี Secret ถูกปฏิเสธ', ENV_WEB + ', ' + ENV_NODE, 'บริการ Node.js ทำงานอยู่', 'TC-UN01.002'],
['TC-UN11.002', 'UN11.002', 'รันงานสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมด/ใบแจ้งหนี้ค้างชำระสองรอบติดกัน; ตรวจเวลาบันทึกเทียบ Asia/Bangkok', 'แถวสรุปยอดของงวดเดียวกันไม่เพิ่มซ้ำในรอบที่สอง แจ้งเตือนส่งเฉพาะบริษัทเจ้าของข้อมูล เวลาตรงกับ Asia/Bangkok', ENV_NODE, 'Scheduler ทำงานอยู่', 'TC-UN04.001, TC-UN07.001'],
['TC-UN12.001', 'UN12.001', 'ผู้ใช้บริษัท A เรียกข้อมูล/รายการของบริษัท B และคลังที่ไม่ได้รับสิทธิ์ ทั้งผ่าน UI และ URL/ API โดยตรง', 'ถูกปฏิเสธทุกช่องทาง ไม่มีข้อมูลรั่วไหลหรือถูกแก้ไข', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN01.002, TC-UN03.001'],
['TC-UN12.002', 'UN12.002', 'ส่งข้อมูลนำเข้าที่มี SQL/Script และค่าที่ไม่ถูกต้องไปยัง Action ฝั่งเซิร์ฟเวอร์', 'ถูกปฏิเสธหรือ Escape โดยไม่มีข้อมูลเปลี่ยนแปลงหรือ Script ทำงาน', ENV_WEB + ', เครื่องมือทดสอบ API', '-', 'TC-UN01.002'],
['TC-UN12.003', 'UN12.003', 'ผ่านรายการลงวันที่นอกช่วงเวลาที่เปิด; ใช้งานเกินจำนวนรายการของแพ็กเกจแล้วเปิดแดชบอร์ด/รายงาน', 'การผ่านรายการนอกช่วงถูกปฏิเสธพร้อมข้อความ แดชบอร์ด/รายงานถูกล็อก (HTTP 402) พร้อมข้อความให้ปรับแพ็กเกจ', ENV_WEB, 'ตั้งช่วงเวลาผ่านรายการและแพ็กเกจทดสอบ', 'TC-UN08.002'],
['TC-UN12.004', 'UN12.004', 'ตรวจ Repository และสภาพแวดล้อมที่ติดตั้งว่ามีความลับหรือไฟล์ตั้งค่าเปิดเผยหรือไม่; เรียกผ่าน HTTPS', 'ไม่พบความลับใน Git หรือเข้าถึงได้จากเว็บ การเชื่อมต่อผ่าน TLS', 'Repository, Deployed Environment', '-', '-'],
['TC-UN13.001', 'UN13.001', 'ติดตั้งบนเครื่องใหม่ด้วย setup.php', 'ฐานข้อมูลและตารางครบตาม SR08 โดยไม่แก้ Schema ด้วยมือ', 'Fresh Linux Server, PHP 8, MariaDB', 'ต้องใช้เครื่องว่าง', '-'],
['TC-UN13.002', 'UN13.002', 'รัน docker compose up -d --build', 'Service db, php, node ทำงานและเข้าใช้ระบบได้', 'Docker Host', '-', 'TC-UN13.003'],
['TC-UN13.003', 'UN13.003', 'รัน docker/init-env.sh โดยเว้นค่าความลับว่าง', '.env ถูกสร้างพร้อมความลับสุ่ม และไม่ถูก Git ติดตาม', 'Docker Host', '-', '-'],
['TC-UN13.004', 'UN13.004', 'สำรองฐานข้อมูลด้วย mysqldump กู้คืนสู่สภาพแวดล้อมทดสอบ และ Clone จาก Backup Remote', 'ข้อมูลและ Source Code กู้คืนได้ครบตามขั้นตอนในคู่มือ', 'Testing Server, Backup Remote', 'ตาม Product Operation Guide', 'TC-UN13.001'],
];
/**
* Results of the system test run (10–14 Aug) on the build under test. Anything not
* listed passed as expected. A failure names the open issue in the Correction Register;
* "Not tested" marks a function delivered after the run.
*/
export const TEST_RESULTS = {
'TC-UN04.006': { status: 'Failed', issue: 'ISS-029', actual: 'อัปโหลดและตรวจชนิด/ขนาดไฟล์ถูกต้อง แต่ไฟล์แนบอยู่ใต้ app/uploads/ ที่เว็บเปิดให้เรียกโดยตรง ผู้ที่ทราบ URL เปิดไฟล์ได้โดยไม่ตรวจ Session และบริษัท' },
'TC-UN08.002': { status: 'Failed', issue: 'ISS-030', actual: 'สมุดรายวันที่ไม่สมดุลถูกปฏิเสธ แต่เมื่อการผ่าน GL ล้มเหลว เอกสารที่บันทึกแล้วไม่ถูก Rollback เพราะการผ่าน GL แยก Transaction จากเอกสาร' },
'TC-UN11.001': { status: 'Failed', issue: 'ISS-031', actual: 'การเรียก Endpoint แจ้งเตือนโดยไม่มี Secret ถูกปฏิเสธ แต่ Socket.IO รับ company_id, user_id และ role จาก Browser โดยไม่ตรวจกับ Session ผู้ที่ปลอมค่าได้รับแจ้งเตือนของบริษัทอื่น' },
'TC-UN12.001': { status: 'Failed', issue: 'ISS-031', actual: 'หน้าจอและ API ปฏิเสธการเข้าถึงข้ามบริษัทถูกต้อง แต่ช่องทาง Socket.IO ยังรับการแจ้งเตือนของบริษัทอื่นได้' },
'TC-UN12.004': { status: 'Failed', issue: 'ISS-032', actual: 'ไม่พบไฟล์ .env หรือรหัสผ่านใน Git แต่แม่แบบค่าตั้งค่ากำหนดกุญแจเข้ารหัสและ IV คงที่ เชื่อมต่อฐานข้อมูลด้วยผู้ใช้ root และไม่มีหลักฐานการตั้งค่า HTTPS/TLS' },
'TC-UN13.002': { status: 'Not tested', actual: 'ชุดติดตั้ง Docker Compose ยังไม่อยู่ใน Build ที่ทดสอบ ส่งมอบภายหลังใน Task 4.7' },
'TC-UN13.003': { status: 'Not tested', actual: 'สคริปต์ docker/init-env.sh ยังไม่อยู่ใน Build ที่ทดสอบ ส่งมอบภายหลังใน Task 4.7' },
};
export const testResult = (tcId) => TEST_RESULTS[tcId] ?? { status: 'Passed', actual: 'ผลตรงตามที่คาดหวัง' };
export const resultCounts = () => {
const counts = { Passed: 0, Failed: 0, 'Not tested': 0 };
for (const [tc] of TEST_CASES) counts[testResult(tc).status] += 1;
return counts;
};
/**
* Requirements met by an operations task or document finished after the test run:
* pending in any record dated before that date.
*/
export const PENDING_UNTIL = {
'CR08:001': ['2026-08-23', 'รอปิดรายการ OP-001 สำรองฐานข้อมูลอัตโนมัติ'],
'CR08:003': ['2026-08-23', 'รอปิดรายการ OP-002 การเฝ้าระวังและแจ้งเตือน'],
'CR11:003': ['2026-08-22', 'รอการอบรมผู้ใช้งานตามแผน'],
'CR12:004': ['2026-08-24', 'รอจัดทำ List of Evidence ตอนปิดโครงการ'],
};
/** Status of a requirement as known on `asOf`: Failed, Not tested, Pending or Passed. */
export const crStatus = (crId, asOf) => {
const t = traceOf(crId);
const results = t.tcs.map((tc) => testResult(tc).status);
if (results.includes('Failed')) return 'Failed';
if (results.includes('Not tested')) return 'Not tested';
const pending = PENDING_UNTIL[crId];
if (pending && asOf < pending[0]) return 'Pending';
return 'Passed';
};
export const tcByUnit = (unitId) => TEST_CASES.find((t) => t[1] === unitId)?.[0] ?? '';
/**
* Traceability: CR id -> { sr: [...], un: [...], ev? }.
* Every unit brings its own test case. Requirements met by a delivered document or record
* (manuals, training, SLA, quality evidence) name it in `ev`, with or without software units.
*/
export const TRACE = {
'CR01:001': { sr: ['SR03:001', 'SR08:001'], un: ['UN01.001'] },
'CR01:002': { sr: ['SR03:001', 'SR04:001', 'SR07:005'], un: ['UN01.002'] },
'CR01:003': { sr: ['SR03:001', 'SR07:003'], un: ['UN01.003'] },
'CR01:004': { sr: ['SR03:001', 'SR03:002'], un: ['UN01.004', 'UN02.001', 'UN02.002'] },
'CR01:005': { sr: ['SR03:003', 'SR08:002'], un: ['UN03.001', 'UN03.002', 'UN03.003'] },
'CR01:006': { sr: ['SR03:003'], un: ['UN03.004'] },
'CR01:007': { sr: ['SR03:004', 'SR08:003'], un: ['UN04.001'] },
'CR01:008': { sr: ['SR03:004', 'SR09:003'], un: ['UN04.002'] },
'CR01:009': { sr: ['SR03:004'], un: ['UN04.003'] },
'CR01:010': { sr: ['SR03:004', 'SR08:002'], un: ['UN04.004'] },
'CR01:011': { sr: ['SR03:009'], un: ['UN09.002'] },
'CR01:012': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR01:013': { sr: ['SR03:005', 'SR04:002'], un: ['UN05.001', 'UN05.002', 'UN05.003', 'UN05.004'] },
'CR01:014': { sr: ['SR03:006', 'SR04:002'], un: ['UN06.001', 'UN06.002', 'UN06.003', 'UN06.004'] },
'CR01:015': { sr: ['SR03:007', 'SR04:002'], un: ['UN07.001', 'UN07.002'] },
'CR01:016': { sr: ['SR03:008'], un: ['UN08.001', 'UN08.002'] },
'CR01:017': { sr: ['SR03:009'], un: ['UN09.003'] },
'CR01:018': { sr: ['SR03:010', 'SR04:005', 'SR08:004'], un: ['UN10.001', 'UN10.002'] },
'CR01:019': { sr: ['SR03:004'], un: ['UN04.006'] },
'CR01:020': { sr: ['SR03:009'], un: ['UN09.004'] },
'CR01:021': { sr: ['SR03:011', 'SR04:004', 'SR06:005'], un: ['UN11.001'] },
'CR01:022': { sr: ['SR03:011', 'SR05:002', 'SR08:004'], un: ['UN11.002'] },
'CR01:023': { sr: ['SR03:010', 'SR09:001'], un: ['UN10.003'] },
'CR01:024': { sr: ['SR04:001', 'SR04:003', 'SR07:002'], un: ['UN12.001'] },
'CR02:001': { sr: ['SR05:001'], un: ['UN09.001'] },
'CR02:002': { sr: ['SR05:002'], un: ['UN09.001', 'UN11.002'] },
'CR02:003': { sr: ['SR05:003'], un: ['UN09.001'] },
'CR03:001': { sr: ['SR06:001'], un: ['UN13.001'] },
'CR03:002': { sr: ['SR06:002'], un: ['UN01.002'] },
'CR03:003': { sr: ['SR06:004', 'SR04:004'], un: ['UN11.001'] },
'CR03:004': { sr: ['SR06:005'], un: ['UN11.001'] },
'CR03:005': { sr: ['SR06:004', 'SR03:002'], un: ['UN02.002'] },
'CR04:001': { sr: ['SR02:001'], un: ['UN01.002'] },
'CR04:002': { sr: ['SR08:001', 'SR08:002', 'SR08:003'], un: ['UN13.001'] },
'CR04:003': { sr: ['SR04:001'], un: ['UN01.002'] },
'CR04:004': { sr: ['SR01:002'], un: ['UN13.001'] },
'CR05:001': { sr: ['SR02:002'], un: ['UN01.002'] },
'CR05:002': { sr: ['SR04:001'], un: ['UN01.002'] },
'CR05:003': { sr: ['SR09:002'], un: ['UN10.002'] },
'CR05:004': { sr: ['SR09:001'], un: ['UN10.002'] },
'CR05:005': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR06:001': { sr: ['SR07:001'], un: ['UN12.004'] },
'CR06:002': { sr: ['SR01:005'], un: ['UN13.003', 'UN12.004'] },
'CR06:003': { sr: ['SR07:002'], un: ['UN12.002', 'UN12.003'] },
'CR06:004': { sr: ['SR07:005'], un: ['UN01.002'] },
'CR06:005': { sr: ['SR07:004'], un: ['UN12.002'] },
'CR06:006': { sr: ['SR07:003'], un: ['UN01.003'] },
'CR07:001': { sr: ['SR01:003'], un: ['UN13.001', 'UN13.002'] },
'CR07:002': { sr: ['SR01:006'], un: ['UN11.002'] },
'CR07:003': { sr: ['SR06:003', 'SR02:002'], un: ['UN01.002'] },
'CR07:004': { sr: ['SR01:003'], un: ['UN13.001'] },
'CR08:001': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR08:002': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR08:003': { sr: ['SR05:004'], un: [], ev: 'Product Operation Guide (การเฝ้าระวังระบบ)' },
'CR08:004': { sr: ['SR04:003'], un: ['UN12.001'] },
'CR08:005': { sr: ['SR05:002'], un: ['UN11.002'] },
'CR09:001': { sr: ['SR02:003', 'SR02:004'], un: [], ev: 'Software Design (Software Unit), Software Components' },
'CR09:002': { sr: ['SR01:005'], un: ['UN13.003'] },
'CR09:003': { sr: ['SR01:001'], un: [], ev: 'Maintenance Document, Correction Register' },
'CR09:004': { sr: ['SR08:005', 'SR08:004'], un: ['UN13.001'] },
'CR10:001': { sr: ['SR01:003', 'SR08:005'], un: ['UN13.001'] },
'CR10:002': { sr: ['SR02:005'], un: ['UN13.002'] },
'CR10:003': { sr: ['SR01:005'], un: ['UN13.003'] },
'CR10:004': { sr: ['SR01:001'], un: ['UN13.001', 'UN13.002'], ev: 'Product Operation Guide (ขั้นตอนติดตั้ง)' },
'CR11:001': { sr: ['SR01:001'], un: [], ev: 'Software User Document (WP 8.0)' },
'CR11:002': { sr: ['SR01:001'], un: [], ev: 'Product Operation Guide (WP 10.0)' },
'CR11:003': { sr: ['SR01:001'], un: [], ev: 'Training Report' },
'CR11:004': { sr: ['SR01:001'], un: [], ev: 'Maintenance Document (SLA)' },
'CR12:001': { sr: ['SR01:002'], un: ['UN13.001'] },
'CR12:002': { sr: ['SR01:001'], un: [], ev: 'Software Configuration, Project Repository' },
'CR12:003': { sr: ['SR01:004'], un: ['UN01.003'] },
'CR12:004': { sr: ['SR01:001'], un: [], ev: 'List of Evidence, Verification Results' },
'CR13:001': { sr: ['SR09:003'], un: ['UN08.002'] },
'CR13:002': { sr: ['SR09:003'], un: ['UN04.002'] },
'CR13:003': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR13:004': { sr: ['SR09:002'], un: ['UN10.002'] },
'CR14:001': { sr: ['SR01:001'], un: [], ev: 'Test Report' },
'CR14:002': { sr: ['SR01:001'], un: [], ev: 'Traceability Record' },
'CR14:003': { sr: ['SR01:001'], un: [], ev: 'Validation Results, Acceptance Report' },
'CR14:004': { sr: ['SR07:002'], un: ['UN12.001', 'UN12.002'], ev: 'Test Report, Correction Register' },
};
export const traceOf = (crId) => {
const t = TRACE[crId];
if (!t) throw new Error(`No traceability entry for ${crId}`);
const tcs = [...new Set(t.un.map(tcByUnit))];
return { sr: t.sr, un: t.un, ev: t.ev ?? '', tcs, tc: tcs.join(', ') || '-' };
};
// Sanity: every CR is traced, every trace target exists, and each CR ends in a test case or named evidence.
for (const [id] of CRS) {
const t = traceOf(id);
for (const s of t.sr) if (!srTopic(s)) throw new Error(`${id} references unknown ${s}`);
for (const u of t.un) if (!unitTopic(u)) throw new Error(`${id} references unknown ${u}`);
if (t.tcs.some((tc) => !tc)) throw new Error(`${id} has a unit without a test case`);
if (!t.tcs.length && !t.ev) throw new Error(`${id} has neither a test case nor evidence`);
}