// 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', 'เพิ่ม/แก้ไข/ปิดใช้ผู้ใช้, กำหนดบทบาทและสิทธิ์การเข้าถึงแอปพลิเคชัน', 'SR03:001, SR08:001', 'app/setting/, 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'], ['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, StockManager.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/, PostingManager.php'], ['UN08.002', 'Accounting – Journal & GL Posting', 'บันทึกสมุดรายวันและผ่านรายการ GL แบบสมดุลภายใน Transaction', 'SR03:008, SR09:003', 'app/journal/, PostingManager.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', 'ReportManager.php'], ['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', 'บันทึกผู้สร้าง ผู้แก้ไข วันที่ และประวัติรายการ', 'SR03:010, SR09:001', '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 – Operation Lock & Usage Guard', 'ล็อกช่วงเวลาผ่านรายการ และจำกัดโควตารายการต่อบริษัท', 'SR07:002, SR09:003', 'OperationLockManager.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', 'Container php-apache, mariadb, node/pm2 พร้อม 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', 'Admin เพิ่มผู้ใช้ เปลี่ยนบทบาท และปิดสิทธิ์แอปพลิเคชัน; Staff ลองทำรายการเดียวกัน', 'การเปลี่ยนแปลงของ Admin ถูกบันทึก Staff ถูกปฏิเสธ', ENV_WEB, '-', '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', 'เปิดแดชบอร์ดคลังสินค้าและบัญชีหลังรันงานสรุปยอด', 'ตัวเลขตรงกับตารางสรุปยอดและแสดงผลภายในเวลาที่ใช้งานได้', 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', 'สร้างแล้วแก้ไขรายการ แล้วเปิดดูประวัติ', 'ผู้สร้าง ผู้แก้ไข วันที่ และสถานะถูกบันทึกครบ', 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', 'งานเสร็จโดยไม่ซ้ำ ผลลัพธ์อยู่ในขอบเขตสิทธิ์ เวลาถูกต้องตามเขตเวลา', 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', 'ผ่านรายการในงวดที่ล็อก; ทำรายการเกินโควตาบริษัท', 'รายการถูกปฏิเสธพร้อมข้อความ', 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', 'Container php-apache, mariadb, node ทำงานและเข้าใช้ระบบได้', 'Docker Host', '-', 'TC-UN13.003'], ['TC-UN13.003', 'UN13.003', 'รัน docker/init-env.sh โดยเว้นค่าความลับว่าง', '.env ถูกสร้างพร้อมความลับสุ่ม และไม่ถูก Git ติดตาม', 'Docker Host', '-', '-'], ['TC-UN13.004', 'UN13.004', 'สำรองฐานข้อมูล กู้คืนสู่สภาพแวดล้อมทดสอบ และ Clone จาก Backup Remote', 'ข้อมูลและ Source Code กู้คืนได้ครบตามขั้นตอนในคู่มือ', 'Testing Server, Backup Remote', 'ตาม Product Operation Guide', 'TC-UN13.001'], ]; 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'] }, '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`); }