Compare commits
37 Commits
main
...
fc6cf11818
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc6cf11818 | ||
|
|
eeff27a511 | ||
|
|
ff0f1884e1 | ||
|
|
2b8e63a7b0 | ||
| 477fe6b287 | |||
| c05e715e03 | |||
| 18f78f8a5e | |||
|
|
474982af87 | ||
| d33663f7a9 | |||
|
|
e04ec1243d | ||
| d74218bb2a | |||
|
|
79344ef4b1 | ||
| 8f4b28519d | |||
|
|
7c32a96dcb | ||
| f54a906bcd | |||
|
|
00b8995f84 | ||
| 047e1b88ce | |||
|
|
ce795b26e2 | ||
| c302c5f9b1 | |||
|
|
138b09d0c8 | ||
|
|
6cafa6a2b9 | ||
| 43f6bd1f40 | |||
| 1883c0bb59 | |||
|
|
f725bd5d3e | ||
|
|
b42c8c0c9f | ||
| bfa8d3df83 | |||
|
|
5fe2ea92ce | ||
| 13c9554be7 | |||
|
|
fa6f6a5fc9 | ||
| 0ce895c96a | |||
|
|
0e5d7e7e9e | ||
|
|
cb6faacba6 | ||
| 3d9b6e4d05 | |||
| 9c970f8ed8 | |||
| fe977ced6d | |||
|
|
7eb9a1a633 | ||
|
|
6d1e2c668c |
@@ -31,9 +31,9 @@ Before generating code or planning a solution, you MUST conceptually load the co
|
||||
- *Crucial:* Check `specs/05-decisions/` (ADRs) to ensure you do not violate previously agreed-upon technical decisions.
|
||||
|
||||
5. **💾 DATABASE & SCHEMA (`specs/07-databasee/`)**
|
||||
- *Action:* - **Read `specs/07-database/lcbp3-v1.6.0-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints.
|
||||
- **Consult `specs/07-database/data-dictionary-v1.6.0.md`** for field meanings and business rules.
|
||||
- **Check `specs/07-database/lcbp3-v1.6.0-seed.sql`** to understand initial data states.
|
||||
- *Action:* - **Read `specs/07-database/lcbp3-v1.5.1-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints.
|
||||
- **Consult `specs/database/data-dictionary-v1.5.1.md`** for field meanings and business rules.
|
||||
- **Check `specs/database/lcbp3-v1.5.1-seed.sql`** to understand initial data states.
|
||||
- *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here.
|
||||
|
||||
6. **⚙️ IMPLEMENTATION DETAILS (`specs/03-implementation/`)**
|
||||
|
||||
10
.aignore
10
.aignore
@@ -1,10 +0,0 @@
|
||||
node_modules/
|
||||
backend/node_modules/
|
||||
frontend/node_modules/
|
||||
backend/dist/
|
||||
frontend/dist/
|
||||
backend/build/
|
||||
frontend/build/
|
||||
docs/backup/
|
||||
.git/
|
||||
*.log
|
||||
@@ -1,6 +1,4 @@
|
||||
node_modules
|
||||
backend/node_modules/
|
||||
frontend/node_modules/
|
||||
dist
|
||||
build
|
||||
*.min.js
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -33,8 +33,6 @@
|
||||
"alefragnani.bookmarks",
|
||||
"pkief.material-icon-theme",
|
||||
"github.copilot",
|
||||
"bierner.markdown-mermaid",
|
||||
"vitest.explorer",
|
||||
"google.geminicodeassist"
|
||||
"bierner.markdown-mermaid"
|
||||
]
|
||||
}
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
32
2git.ps1
32
2git.ps1
@@ -1,38 +1,14 @@
|
||||
param([string]$Message = "Backup")
|
||||
|
||||
$Timestamp = Get-Date -Format "yyMMdd:HHmm"
|
||||
$CommitMsg = "$Timestamp $Message"
|
||||
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$CommitMsg = "Backup: $Message | $Timestamp"
|
||||
|
||||
Write-Host "📦 $CommitMsg" -ForegroundColor Cyan
|
||||
Write-Host "Backup: $CommitMsg" -ForegroundColor Cyan
|
||||
|
||||
git add .
|
||||
|
||||
# Check if anything to commit
|
||||
$status = git status --porcelain
|
||||
if (-not $status) {
|
||||
Write-Host "⚠️ Nothing to commit" -ForegroundColor Yellow
|
||||
pause
|
||||
exit
|
||||
}
|
||||
|
||||
git commit -m $CommitMsg
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Commit failed" -ForegroundColor Red
|
||||
pause
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "🚀 Pushing to Gitea..." -ForegroundColor Cyan
|
||||
git push origin main
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Push to Gitea failed" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "🚀 Pushing to GitHub..." -ForegroundColor Cyan
|
||||
git push github main
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Push to GitHub failed" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "✅ Done!" -ForegroundColor Green
|
||||
Write-Host "Done!" -ForegroundColor Green
|
||||
pause
|
||||
|
||||
185
CHANGELOG.md
185
CHANGELOG.md
@@ -1,191 +1,16 @@
|
||||
# Version History
|
||||
# Version history
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### In Progress
|
||||
- Backend Document Numbering Refactor (TASK-BE-017)
|
||||
- E2E Testing & UAT preparation
|
||||
- Production deployment preparation
|
||||
|
||||
## 1.7.0 (2025-12-18)
|
||||
## 1.4.5 (2025-11-28)
|
||||
|
||||
### Summary
|
||||
**Schema Stabilization & Document Numbering Overhaul** - Significant schema updates to support advanced document numbering (reservations, varying reset scopes) and a unified workflow engine.
|
||||
|
||||
### Database Schema Changes 💾
|
||||
|
||||
#### Document Numbering System (V2) 🔢
|
||||
- **`document_number_counters`**:
|
||||
- **Breaking Change**: Primary Key changed to 8-column Composite Key (`project_id`, `originator_id`, `recipient_id`, `type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `reset_scope`).
|
||||
- **New Feature**: Added `reset_scope` column to support flexible resetting (YEAR, MONTH, PROJECT, NONE).
|
||||
- **New Feature**: Added `version` column for Optimistic Locking.
|
||||
- **`document_number_reservations`** (NEW):
|
||||
- Implemented Two-Phase Commit pattern (Reserve -> Confirm) for document numbers.
|
||||
- Prevents race conditions and gaps in numbering.
|
||||
- **`document_number_errors`** (NEW):
|
||||
- Helper table for tracking numbering failures and deadlocks.
|
||||
- **`document_number_audit`**:
|
||||
- Enhanced with reservation tokens and performance metrics.
|
||||
|
||||
#### Unified Workflow Engine 🔄
|
||||
- **`workflow_definitions`**:
|
||||
- Updated structure to support compiled DSL and versioning.
|
||||
- Added `dsl` (JSON) and `compiled` (JSON) columns.
|
||||
- **`workflow_instances`**:
|
||||
- Changed ID to UUID.
|
||||
- Added `entity_type` and `entity_id` for polymorphic polymorphism.
|
||||
- **`workflow_histories`**:
|
||||
- Updated to link with UUID instances.
|
||||
|
||||
#### System & Audit 🛡️
|
||||
- **`audit_logs`**:
|
||||
- Updated schema for better partitioning support (`created_at` in PK).
|
||||
- Standardized JSON details column.
|
||||
- **`notifications`**:
|
||||
- Updated schema to support polymorphic entity linking.
|
||||
|
||||
#### Master Data
|
||||
- **`disciplines`**:
|
||||
- Added relation to `correspondences` and `rfas`.
|
||||
|
||||
### Documentation 📚
|
||||
- **Data Dictionary**: Updated to v1.7.0 with full index summaries and business rules.
|
||||
- **Schema**: Released `lcbp3-v1.7.0-schema.sql` and `lcbp3-v1.7.0-seed.sql`.
|
||||
|
||||
## 1.6.0 (2025-12-13)
|
||||
|
||||
### Summary
|
||||
**Schema Refactoring Release** - Major restructuring of correspondence and RFA tables for improved data consistency.
|
||||
|
||||
### Database Schema Changes 💾
|
||||
|
||||
#### Breaking Changes ⚠️
|
||||
- **`correspondence_recipients`**: FK changed from `correspondence_revisions(correspondence_id)` → `correspondences(id)`
|
||||
- **`rfa_items`**: Column renamed `rfarev_correspondence_id` → `rfa_revision_id`
|
||||
|
||||
#### Schema Refactoring
|
||||
- **`correspondences`**: Reordered columns, `discipline_id` now inline (no ALTER TABLE)
|
||||
- **`correspondence_revisions`**:
|
||||
- Renamed: `title` → `subject`
|
||||
- Added: `body TEXT`, `remarks TEXT`, `schema_version INT`
|
||||
- Added Virtual Columns: `v_ref_project_id`, `v_doc_subtype`
|
||||
- **`rfas`**:
|
||||
- Changed to Shared PK pattern (no AUTO_INCREMENT)
|
||||
- PK now FK to `correspondences(id)`
|
||||
- **`rfa_revisions`**:
|
||||
- Removed: `correspondence_id` (uses rfas.id instead)
|
||||
- Renamed: `title` → `subject`
|
||||
- Added: `body TEXT`, `remarks TEXT`, `due_date DATETIME`, `schema_version INT`
|
||||
- Added Virtual Column: `v_ref_drawing_count`
|
||||
|
||||
### Documentation 📚
|
||||
- Updated Data Dictionary to v1.6.0
|
||||
- Updated schema SQL files (`lcbp3-v1.6.0-schema.sql`, seed files)
|
||||
|
||||
## 1.5.1 (2025-12-10)
|
||||
|
||||
### Summary
|
||||
**Major Milestone: System Feature Complete (~95%)** - Ready for UAT and production deployment.
|
||||
|
||||
All core modules implemented and operational. Backend and frontend fully integrated with comprehensive admin tools.
|
||||
|
||||
### Backend Completed ✅
|
||||
|
||||
#### Core Infrastructure
|
||||
- ✅ All 18 core modules implemented and tested
|
||||
- ✅ JWT Authentication with Refresh Token mechanism
|
||||
- ✅ RBAC 4-Level (Global, Organization, Project, Contract) using CASL
|
||||
- ✅ Document Numbering with Redis Redlock + Optimistic Locking
|
||||
- ✅ Workflow Engine (DSL-based Hybrid Engine with legacy support)
|
||||
- ✅ Two-Phase File Storage with ClamAV Virus Scanning
|
||||
- ✅ Global Audit Logging with Interceptor
|
||||
- ✅ Health Monitoring & Metrics endpoints
|
||||
|
||||
#### Business Modules
|
||||
- ✅ **Correspondence Module** - Master-Revision pattern, Workflow integration, References
|
||||
- ✅ **RFA Module** - Full CRUD, Item management, Revision handling, Approval workflow
|
||||
- ✅ **Drawing Module** - Separated into Shop Drawing & Contract Drawing
|
||||
- ✅ **Transmittal Module** - Document transmittal tracking
|
||||
- ✅ **Circulation Module** - Circulation sheet management
|
||||
- ✅ **Elasticsearch Integration** - Direct indexing, Full-text search (95% complete)
|
||||
|
||||
#### Supporting Services
|
||||
- ✅ **Notification System** - Email and LINE notification integration
|
||||
- ✅ **Master Data Management** - Consolidated service for Organizations, Projects, Disciplines, Types
|
||||
- ✅ **User Management** - CRUD, Assignments, Preferences, Soft Delete
|
||||
- ✅ **Dashboard Service** - Statistics and reporting APIs
|
||||
- ✅ **JSON Schema Validation** - Dynamic schema validation for documents
|
||||
|
||||
### Frontend Completed ✅
|
||||
|
||||
#### Application Structure
|
||||
- ✅ All 15 frontend tasks (FE-001 to FE-015) completed
|
||||
- ✅ Next.js 14 App Router with TypeScript
|
||||
- ✅ Complete UI implementation (17 component groups, 22 Shadcn/UI components)
|
||||
- ✅ TanStack Query for server state management
|
||||
- ✅ Zustand for client state management
|
||||
- ✅ React Hook Form + Zod for form validation
|
||||
- ✅ Responsive layout (Desktop & Mobile)
|
||||
|
||||
#### End-User Modules
|
||||
- ✅ **Authentication UI** - Login, Token Management, Session Sync
|
||||
- ✅ **RBAC UI** - `<Can />` component for permission-based rendering
|
||||
- ✅ **Correspondence UI** - List, Create, Detail views with file uploads
|
||||
- ✅ **RFA UI** - List, Create, Item management
|
||||
- ✅ **Drawing UI** - Contract & Shop drawing lists, Upload forms
|
||||
- ✅ **Search UI** - Global search bar, Advanced filtering with Elasticsearch
|
||||
- ✅ **Dashboard** - Real-time KPI cards, Activity feed, Pending tasks
|
||||
- ✅ **Circulation UI** - Circulation sheet management with DataTable
|
||||
- ✅ **Transmittal UI** - Transmittal tracking and management
|
||||
|
||||
#### Admin Panel (10 Routes)
|
||||
- ✅ **Workflow Configuration** - DSL Editor, Visual Builder, Workflow Definition management
|
||||
- ✅ **Document Numbering Config** - Template Editor, Token Tester, Sequence Viewer
|
||||
- ✅ **User Management** - CRUD, Role assignments, Preferences
|
||||
- ✅ **Organization Management** - Organization CRUD and hierarchy
|
||||
- ✅ **Project Management** - Project and contract administration
|
||||
- ✅ **Reference Data Management** - CRUD for Disciplines, Types, Categories (6 modules)
|
||||
- ✅ **Security Administration** - RBAC Matrix, Roles, Active Sessions (2 modules)
|
||||
- ✅ **Audit Logs** - Comprehensive audit log viewer
|
||||
- ✅ **System Logs** - System log monitoring
|
||||
- ✅ **Settings** - System configuration
|
||||
|
||||
### Database 💾
|
||||
- ✅ Schema v1.5.1 with standardized audit columns (`created_at`, `updated_at`, `deleted_at`)
|
||||
- ✅ Complete seed data for all master tables
|
||||
- ✅ Migration scripts and patches (`patch-audit-columns.sql`)
|
||||
- ✅ Data Dictionary v1.5.1 documentation
|
||||
|
||||
### Documentation 📚
|
||||
- ✅ Complete specs/ reorganization to v1.5.1
|
||||
- ✅ 21 requirements documents in `specs/01-requirements/`
|
||||
- ✅ 17 ADRs (Architecture Decision Records) in `specs/05-decisions/`
|
||||
- ✅ Implementation guides for Backend & Frontend
|
||||
- ✅ Operations guides for critical features (Document Numbering)
|
||||
- ✅ Comprehensive progress reports updated
|
||||
- ✅ Task archiving to `specs/09-history/` (27 completed tasks)
|
||||
|
||||
### Bug Fixes 🐛
|
||||
- 🐛 Fixed role selection bug in User Edit form (2025-12-09)
|
||||
- 🐛 Fixed workflow permissions - 403 error on workflow action endpoints
|
||||
- 🐛 Fixed TypeORM relation errors in RFA and Drawing services
|
||||
- 🐛 Fixed token refresh infinite loop in authentication
|
||||
- 🐛 Fixed database schema alignment issues (audit columns)
|
||||
- 🐛 Fixed "drawings.map is not a function" by handling paginated responses
|
||||
- 🐛 Fixed invalid refresh token error loop
|
||||
|
||||
### Changed 📝
|
||||
- 📝 Updated progress reports to reflect ~95% backend, 100% frontend completion
|
||||
- 📝 Aligned all TypeORM entities with schema v1.5.1
|
||||
- 📝 Enhanced data dictionary with business rules
|
||||
- 📝 Archived 27 completed task files to `specs/09-history/`
|
||||
- Backend development 80% remaining test tasks
|
||||
|
||||
## 1.5.0 (2025-11-30)
|
||||
|
||||
### Summary
|
||||
Initial spec-kit structure establishment and documentation organization.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the version to 1.5.0
|
||||
- Modified to Spec-kit
|
||||
|
||||
### Summary
|
||||
|
||||
@@ -20,19 +20,18 @@
|
||||
|
||||
## 🗂️ Specification Structure
|
||||
|
||||
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 9 หมวดหลัก:
|
||||
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 6 หมวดหลัก:
|
||||
|
||||
```
|
||||
specs/
|
||||
├── 00-overview/ # ภาพรวมโครงการ (3 docs)
|
||||
│ ├── README.md # Project overview
|
||||
│ ├── glossary.md # คำศัพท์เทคนิค
|
||||
│ └── quick-start.md # Quick start guide
|
||||
├── 00-overview/ # ภาพรวมโครงการ
|
||||
│ ├── README.md # Project overview
|
||||
│ └── glossary.md # คำศัพท์เทคนิค
|
||||
│
|
||||
├── 01-requirements/ # ข้อกำหนดระบบ (21 docs)
|
||||
│ ├── README.md # Requirements overview
|
||||
│ ├── 01-objectives.md # วัตถุประสงค์
|
||||
│ ├── 02-architecture.md # สถาปัตยกรรม
|
||||
├── 01-requirements/ # ข้อกำหนดระบบ
|
||||
│ ├── README.md # Requirements overview
|
||||
│ ├── 01-objectives.md # วัตถุประสงค์
|
||||
│ ├── 02-architecture.md # สถาปัตยกรรม
|
||||
│ ├── 03-functional-requirements.md
|
||||
│ ├── 03.1-project-management.md
|
||||
│ ├── 03.2-correspondence.md
|
||||
@@ -51,59 +50,39 @@ specs/
|
||||
│ ├── 06-non-functional.md
|
||||
│ └── 07-testing.md
|
||||
│
|
||||
├── 02-architecture/ # สถาปัตยกรรมระบบ (4 docs)
|
||||
├── 02-architecture/ # สถาปัตยกรรมระบบ
|
||||
│ ├── README.md
|
||||
│ ├── system-architecture.md
|
||||
│ ├── api-design.md
|
||||
│ └── data-model.md
|
||||
│
|
||||
├── 03-implementation/ # แผนการพัฒนา (5 docs)
|
||||
├── 03-implementation/ # แผนการพัฒนา
|
||||
│ ├── README.md
|
||||
│ ├── backend-guidelines.md
|
||||
│ ├── frontend-guidelines.md
|
||||
│ ├── testing-strategy.md
|
||||
│ └── code-standards.md
|
||||
│ ├── backend-plan.md
|
||||
│ ├── frontend-plan.md
|
||||
│ └── integration-plan.md
|
||||
│
|
||||
├── 04-operations/ # การดำเนินงาน (9 docs)
|
||||
├── 04-operations/ # การดำเนินงาน
|
||||
│ ├── README.md
|
||||
│ ├── deployment.md
|
||||
│ ├── monitoring.md
|
||||
│ └── ...
|
||||
│ └── monitoring.md
|
||||
│
|
||||
├── 05-decisions/ # Architecture Decision Records (17 ADRs)
|
||||
│ ├── README.md
|
||||
│ ├── ADR-001-workflow-engine.md
|
||||
│ ├── ADR-002-document-numbering.md
|
||||
│ └── ...
|
||||
│
|
||||
├── 06-tasks/ # Active Tasks & Progress (34 files)
|
||||
│ ├── frontend-progress-report.md
|
||||
│ ├── backend-progress-report.md
|
||||
│ └── ...
|
||||
│
|
||||
├── 07-database/ # Database Schema (8 files)
|
||||
│ ├── lcbp3-v1.7.0-schema.sql
|
||||
│ ├── lcbp3-v1.7.0-seed.sql
|
||||
│ ├── data-dictionary-v1.7.0.md
|
||||
│ └── ...
|
||||
│
|
||||
└── 09-history/ # Archived Implementations (9 files)
|
||||
└── ...
|
||||
└── 05-decisions/ # Architecture Decision Records
|
||||
├── README.md
|
||||
├── 001-workflow-engine.md
|
||||
└── 002-file-storage.md
|
||||
```
|
||||
|
||||
### 📋 หมวดหมู่เอกสาร
|
||||
|
||||
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
||||
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
||||
| --------------------- | ----------------------------- | ----------------------------- |
|
||||
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
||||
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
||||
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
||||
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
||||
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
||||
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
||||
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
||||
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
||||
| **04-operations** | Deployment และ Operations | DevOps Team |
|
||||
| **05-decisions** | Architecture Decision Records | Tech Lead + Senior Developers |
|
||||
| **06-tasks** | Active Tasks & Progress | All Team Members |
|
||||
| **07-database** | Database Schema & Seed Data | Backend Lead + DBA |
|
||||
| **09-history** | Archived Implementations | Tech Lead |
|
||||
|
||||
---
|
||||
|
||||
@@ -475,11 +454,11 @@ Then [expected result]
|
||||
|
||||
### Review Levels
|
||||
|
||||
| Level | Reviewer | Scope |
|
||||
| ------------------------ | --------------- | ------------------------------- |
|
||||
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
||||
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
||||
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
||||
| Level | Reviewer | Scope |
|
||||
|-------|----------|-------|
|
||||
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
||||
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
||||
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
||||
|
||||
### Review Timeline
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# NAP-DMS Project Context & Rules
|
||||
|
||||
## 🧠 Role & Persona
|
||||
@@ -18,8 +14,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
||||
|
||||
## 💻 Tech Stack & Constraints
|
||||
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, React Context / Zustand, React Hook Form + Zod, Axios.
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis (BullMQ).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI.
|
||||
- **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
|
||||
|
||||
## 🛡️ Security & Integrity Rules
|
||||
@@ -31,8 +27,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
||||
|
||||
## workflow Guidelines
|
||||
|
||||
- When implementing strictly follow the documents in `specs/`.
|
||||
- Always verify database schema against `specs/07-database/` before writing queries.
|
||||
- When implementing **Workflow Engine**, strictly follow the **DSL** design in `2_Backend_Plan_V1_4_4.Phase6A.md`.
|
||||
- Always verify database schema against `4_Data_Dictionary_V1_4_4.md` before writing queries.
|
||||
|
||||
## 🚫 Forbidden Actions
|
||||
|
||||
200
README.md
200
README.md
@@ -4,23 +4,9 @@
|
||||
>
|
||||
> ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
||||
|
||||
[](./CHANGELOG.md)
|
||||
[](./CHANGELOG.md)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 📈 Current Status (As of 2025-12-18)
|
||||
|
||||
**Overall Progress: ~97% Feature Complete - Production Ready Preparation**
|
||||
|
||||
- ✅ **Backend**: Core modules implemented, refactoring for v1.7.0 Schema
|
||||
- ✅ **Frontend**: UI tasks completed (100%), integrating new v1.7.0 features
|
||||
- ✅ **Database**: Schema v1.7.0 active (Stabilized for Production)
|
||||
- ✅ **Documentation**: Comprehensive specs/ at v1.7.0
|
||||
- ✅ **Admin Tools**: Unified Workflow & Advanced Numbering Config
|
||||
- 🔄 **Testing**: E2E tests and UAT preparation
|
||||
- 📋 **Next**: Final Security Audit & Deployment
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
@@ -55,7 +41,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
||||
```typescript
|
||||
{
|
||||
"framework": "NestJS (TypeScript, ESM)",
|
||||
"database": "MariaDB 11.8",
|
||||
"database": "MariaDB 10.11",
|
||||
"orm": "TypeORM",
|
||||
"authentication": "JWT + Passport",
|
||||
"authorization": "CASL (RBAC)",
|
||||
@@ -125,7 +111,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
||||
- **Node.js**: v20.x หรือสูงกว่า
|
||||
- **pnpm**: v8.x หรือสูงกว่า
|
||||
- **Docker**: v24.x หรือสูงกว่า
|
||||
- **MariaDB**: 11.8
|
||||
- **MariaDB**: 10.11
|
||||
- **Redis**: 7.x
|
||||
|
||||
### การติดตั้ง
|
||||
@@ -208,88 +194,46 @@ Superadmin:
|
||||
|
||||
```
|
||||
lcbp3-dms/
|
||||
├── backend/ # 🔧 NestJS Backend
|
||||
├── backend/ # NestJS Backend
|
||||
│ ├── src/
|
||||
│ │ ├── common/ # Shared utilities, guards, decorators
|
||||
│ │ ├── config/ # Configuration module
|
||||
│ │ ├── database/ # Database entities & migrations
|
||||
│ │ ├── modules/ # Feature modules (17 modules)
|
||||
│ │ │ ├── auth/ # JWT Authentication
|
||||
│ │ │ ├── user/ # User management & RBAC
|
||||
│ │ │ ├── project/ # Project & Contract management
|
||||
│ │ │ ├── correspondence/ # Correspondence module
|
||||
│ │ │ ├── rfa/ # Request for Approval
|
||||
│ │ │ ├── drawing/ # Contract & Shop Drawings
|
||||
│ │ │ ├── workflow-engine/# DSL Workflow Engine
|
||||
│ │ │ ├── document-numbering/ # Auto numbering
|
||||
│ │ │ ├── transmittal/ # Transmittal management
|
||||
│ │ │ ├── circulation/ # Circulation sheets
|
||||
│ │ │ ├── search/ # Elasticsearch integration
|
||||
│ │ │ ├── dashboard/ # Statistics & reporting
|
||||
│ │ │ ├── notification/ # Email/LINE notifications
|
||||
│ │ │ ├── monitoring/ # Health checks & metrics
|
||||
│ │ │ ├── master/ # Master data management
|
||||
│ │ │ ├── organizations/ # Organization management
|
||||
│ │ │ └── json-schema/ # JSON Schema validation
|
||||
│ │ └── main.ts
|
||||
│ ├── test/ # Unit & E2E tests
|
||||
│ └── package.json
|
||||
│
|
||||
├── frontend/ # 🎨 Next.js Frontend
|
||||
│ ├── app/ # App Router
|
||||
│ │ ├── (admin)/ # Admin panel routes
|
||||
│ │ │ └── admin/
|
||||
│ │ │ ├── workflows/ # Workflow configuration
|
||||
│ │ │ ├── numbering/ # Document numbering config
|
||||
│ │ │ ├── users/ # User management
|
||||
│ │ │ └── ...
|
||||
│ │ ├── (auth)/ # Authentication pages
|
||||
│ │ ├── (dashboard)/ # Main dashboard routes
|
||||
│ │ │ ├── correspondences/
|
||||
│ │ │ ├── rfas/
|
||||
│ │ │ ├── drawings/
|
||||
│ │ ├── common/ # Shared modules
|
||||
│ │ ├── modules/ # Feature modules
|
||||
│ │ │ ├── auth/
|
||||
│ │ │ ├── user/
|
||||
│ │ │ ├── project/
|
||||
│ │ │ ├── correspondence/
|
||||
│ │ │ ├── rfa/
|
||||
│ │ │ ├── drawing/
|
||||
│ │ │ ├── workflow-engine/
|
||||
│ │ │ └── ...
|
||||
│ │ └── api/ # API routes (NextAuth)
|
||||
│ ├── components/ # React Components (15 groups)
|
||||
│ │ ├── ui/ # Shadcn/UI components
|
||||
│ │ ├── layout/ # Layout components
|
||||
│ │ ├── common/ # Shared components
|
||||
│ │ ├── correspondences/ # Correspondence UI
|
||||
│ │ ├── rfas/ # RFA UI
|
||||
│ │ ├── drawings/ # Drawing UI
|
||||
│ │ ├── workflows/ # Workflow builder
|
||||
│ │ ├── numbering/ # Numbering config UI
|
||||
│ │ ├── dashboard/ # Dashboard widgets
|
||||
│ │ ├── search/ # Search components
|
||||
│ │ └── ...
|
||||
│ ├── lib/ # Utilities & API clients
|
||||
│ │ ├── api/ # API client functions
|
||||
│ │ ├── services/ # Business logic services
|
||||
│ │ └── stores/ # Zustand state stores
|
||||
│ ├── types/ # TypeScript definitions
|
||||
│ │ └── main.ts
|
||||
│ ├── test/
|
||||
│ └── package.json
|
||||
│
|
||||
├── specs/ # 📘 Project Specifications (v1.5.1)
|
||||
│ ├── 00-overview/ # Project overview & glossary
|
||||
│ ├── 01-requirements/ # Functional requirements (21 docs)
|
||||
│ ├── 02-architecture/ # System architecture
|
||||
│ ├── 03-implementation/ # Implementation guidelines
|
||||
│ ├── 04-operations/ # Deployment & operations
|
||||
│ ├── 05-decisions/ # ADRs (17 decisions)
|
||||
│ ├── 06-tasks/ # Active tasks & progress
|
||||
│ ├── 07-database/ # Schema v1.5.1 & seed data
|
||||
│ └── 09-history/ # Archived implementations
|
||||
├── frontend/ # Next.js Frontend
|
||||
│ ├── app/ # App Router
|
||||
│ ├── components/ # React Components
|
||||
│ ├── lib/ # Utilities
|
||||
│ └── package.json
|
||||
│
|
||||
├── docs/ # 📚 Legacy documentation
|
||||
├── diagrams/ # 📊 Architecture diagrams
|
||||
├── infrastructure/ # 🐳 Docker & Deployment configs
|
||||
├── docs/ # 📚 Legacy documentation
|
||||
│ └── ...
|
||||
│
|
||||
├── .gemini/ # 🤖 AI agent configuration
|
||||
├── .agent/ # Agent workflows
|
||||
├── GEMINI.md # AI coding guidelines
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── CHANGELOG.md # Version history
|
||||
└── pnpm-workspace.yaml # Monorepo configuration
|
||||
├── specs/ # 📘 Project Specifications (v1.5.1)
|
||||
│ ├── 00-overview/ # Project overview & glossary
|
||||
│ ├── 01-requirements/ # Functional requirements
|
||||
│ ├── 02-architecture/ # System architecture & ADRs
|
||||
│ ├── 03-implementation/ # Implementation guidelines
|
||||
│ ├── 04-operations/ # Deployment & operations
|
||||
│ ├── 05-decisions/ # Architecture Decision Records
|
||||
│ ├── 06-tasks/ # Active tasks
|
||||
│ ├── 07-database/ # Database schema & seed data
|
||||
│ └── 09-history/ # Implementation history
|
||||
│
|
||||
├── infrastructure/ # Docker & Deployment
|
||||
│ └── Markdown/ # Legacy docs
|
||||
│
|
||||
└── pnpm-workspace.yaml
|
||||
```
|
||||
|
||||
---
|
||||
@@ -304,16 +248,16 @@ lcbp3-dms/
|
||||
| **Requirements** | ข้อกำหนดระบบและฟังก์ชันการทำงาน | `specs/01-requirements/` |
|
||||
| **Architecture** | สถาปัตยกรรมระบบ, ADRs | `specs/02-architecture/` |
|
||||
| **Implementation** | แนวทางการพัฒนา Backend/Frontend | `specs/03-implementation/` |
|
||||
| **Database** | Schema v1.7.0 + Seed Data | `specs/07-database/` |
|
||||
| **Database** | Schema v1.5.1 + Seed Data | `specs/07-database/` |
|
||||
|
||||
### Schema & Seed Data
|
||||
|
||||
```bash
|
||||
# Import schema
|
||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.7.0-schema.sql
|
||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-schema.sql
|
||||
|
||||
# Import seed data
|
||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.7.0-seed.sql
|
||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-seed.sql
|
||||
```
|
||||
|
||||
### Legacy Documentation
|
||||
@@ -522,7 +466,7 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
||||
|
||||
สำหรับคำถามหรือปัญหา กรุณาติดต่อ:
|
||||
|
||||
- **Email**: <support@np-dms.work>
|
||||
- **Email**: support@np-dms.work
|
||||
- **Internal Chat**: [ระบุช่องทาง]
|
||||
- **Issue Tracker**: [Gitea Issues](https://git.np-dms.work/lcbp3/lcbp3-dms/issues)
|
||||
|
||||
@@ -530,56 +474,26 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
### Version 1.5.1 (Current - Dec 2025) ✅ **FEATURE COMPLETE**
|
||||
### Version 1.5.1 (Current - Dec 2025)
|
||||
|
||||
**Backend (18 Modules - ~95%)**
|
||||
- ✅ Core Infrastructure (Auth, RBAC, File Storage)
|
||||
- ✅ Authentication & Authorization (JWT + CASL RBAC 4-Level)
|
||||
- ✅ Core Infrastructure
|
||||
- ✅ Authentication & Authorization (JWT + CASL RBAC)
|
||||
- ✅ **CASL RBAC 4-Level** - Global, Org, Project, Contract
|
||||
- ✅ **Workflow DSL Parser** - Zod validation & state machine
|
||||
- ✅ Correspondence Module (Master-Revision Pattern)
|
||||
- ✅ RFA Module (Full CRUD + Workflow)
|
||||
- ✅ Drawing Module (Contract + Shop Drawings)
|
||||
- ✅ Workflow Engine (DSL-based Hybrid)
|
||||
- ✅ Document Numbering (Redlock + Optimistic Locking)
|
||||
- ✅ Search (Elasticsearch Direct Indexing)
|
||||
- ✅ Transmittal & Circulation Modules
|
||||
- ✅ Notification & Audit Systems
|
||||
- ✅ Master Data Management
|
||||
- ✅ User Management
|
||||
- ✅ Dashboard & Monitoring
|
||||
- ✅ **Document Number Audit** - Compliance tracking
|
||||
- ✅ **All Token Types** - Including {RECIPIENT}
|
||||
- 🔄 RFA Module
|
||||
- 🔄 Drawing Module
|
||||
- ✅ Swagger API Documentation
|
||||
|
||||
**Frontend (15 Tasks - 100%)**
|
||||
- ✅ Complete UI Implementation (17 component groups)
|
||||
- ✅ All Business Modules (Correspondence, RFA, Drawings)
|
||||
- ✅ Admin Panel (10 routes including Workflow & Numbering Config)
|
||||
- ✅ Dashboard with Real-time Statistics
|
||||
- ✅ Advanced Search UI
|
||||
- ✅ RBAC Permission UI
|
||||
- ✅ Responsive Layout (Desktop & Mobile)
|
||||
### Version 1.6.0 (Planned)
|
||||
|
||||
**Documentation**
|
||||
- ✅ Complete specs/ v1.6.0 (21 requirements, 17 ADRs)
|
||||
- ✅ Database Schema v1.6.0 with seed data
|
||||
- ✅ Implementation & Operations Guides
|
||||
|
||||
### Version 1.7.0 (Current - Dec 2025)
|
||||
|
||||
**Schema & Core Stabilization**
|
||||
- ✅ **Document Numbering V2**: Composite Keys, Reservations, Reset Scopes
|
||||
- ✅ **Unified Workflow Engine**: Compiled DSL, Polymorphic Instances
|
||||
- ✅ **Data Dictionary**: Complete Update to v1.7.0
|
||||
- 🔄 **Backend Refactor**: Aligning services with new schema
|
||||
- 📋 **E2E Test Coverage**: Playwright/Cypress rollout
|
||||
|
||||
### Version 1.8.0+ (Planned - Q1 2026)
|
||||
|
||||
**Production Enhancements**
|
||||
- 📊 Advanced Reporting & Analytics Dashboard
|
||||
- 🔔 Enhanced Notifications (Real-time WebSocket)
|
||||
- 📈 Prometheus Metrics & Grafana Dashboards
|
||||
- 🔍 Queue-based Elasticsearch Indexing
|
||||
- 🚀 Performance Optimization & Caching Strategy
|
||||
- 📱 Mobile App (React Native)
|
||||
- 📋 Advanced Reporting
|
||||
- 📊 Dashboard Analytics
|
||||
- 🔔 Enhanced Notifications (LINE/Email)
|
||||
- 🔄 E2E Tests for Critical APIs
|
||||
- 📈 Prometheus Metrics
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
> backend@1.5.1 build
|
||||
> nest build
|
||||
|
||||
documentation/template-playground/hbs-render.service.ts:1:28 - error TS2307: Cannot find module '@angular/core' or its corresponding type declarations.
|
||||
|
||||
1 import { Injectable } from '@angular/core';
|
||||
~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/hbs-render.service.ts:175:42 - error TS18046: 'error' is of type 'unknown'.
|
||||
|
||||
175 <p><strong>Error:</strong> ${error.message}</p>
|
||||
~~~~~
|
||||
documentation/template-playground/main.ts:1:40 - error TS2307: Cannot find module '@angular/platform-browser-dynamic' or its corresponding type declarations.
|
||||
|
||||
1 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/main.ts:8:12 - error TS7006: Parameter 'err' implicitly has an 'any' type.
|
||||
|
||||
8 .catch(err => console.error('Error starting template playground:', err));
|
||||
~~~
|
||||
documentation/template-playground/template-editor.service.ts:1:28 - error TS2307: Cannot find module '@angular/core' or its corresponding type declarations.
|
||||
|
||||
1 import { Injectable } from '@angular/core';
|
||||
~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.component.ts:1:69 - error TS2307: Cannot find module '@angular/core' or its corresponding type declarations.
|
||||
|
||||
1 import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
||||
~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.component.ts:2:28 - error TS2307: Cannot find module '@angular/common/http' or its corresponding type declarations.
|
||||
|
||||
2 import { HttpClient } from '@angular/common/http';
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.module.ts:1:26 - error TS2307: Cannot find module '@angular/core' or its corresponding type declarations.
|
||||
|
||||
1 import { NgModule } from '@angular/core';
|
||||
~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.module.ts:2:31 - error TS2307: Cannot find module '@angular/platform-browser' or its corresponding type declarations.
|
||||
|
||||
2 import { BrowserModule } from '@angular/platform-browser';
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.module.ts:3:30 - error TS2307: Cannot find module '@angular/common' or its corresponding type declarations.
|
||||
|
||||
3 import { CommonModule } from '@angular/common';
|
||||
~~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.module.ts:4:29 - error TS2307: Cannot find module '@angular/forms' or its corresponding type declarations.
|
||||
|
||||
4 import { FormsModule } from '@angular/forms';
|
||||
~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/template-playground.module.ts:5:34 - error TS2307: Cannot find module '@angular/common/http' or its corresponding type declarations.
|
||||
|
||||
5 import { HttpClientModule } from '@angular/common/http';
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
documentation/template-playground/zip-export.service.ts:1:28 - error TS2307: Cannot find module '@angular/core' or its corresponding type declarations.
|
||||
|
||||
1 import { Injectable } from '@angular/core';
|
||||
~~~~~~~~~~~~~~~
|
||||
src/modules/rfa/rfa.service.ts:422:11 - error TS2339: Property 'returnToSequence' does not exist on type 'WorkflowActionDto'.
|
||||
|
||||
422 dto.returnToSequence
|
||||
~~~~~~~~~~~~~~~~
|
||||
src/modules/rfa/rfa.service.ts:435:37 - error TS2551: Property 'comments' does not exist on type 'WorkflowActionDto'. Did you mean 'comment'?
|
||||
|
||||
435 currentRouting.comments = dto.comments;
|
||||
~~~~~~~~
|
||||
|
||||
src/modules/correspondence/dto/workflow-action.dto.ts:29:3
|
||||
29 comment?: string;
|
||||
~~~~~~~
|
||||
'comment' is declared here.
|
||||
|
||||
Found 15 error(s).
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
|
||||
> backend@1.5.1 build
|
||||
> nest build
|
||||
|
||||
src/modules/document-numbering/controllers/document-numbering.controller.ts:93:37 - error TS2551: Property 'originatorOrganizationId' does not exist on type 'PreviewNumberDto'. Did you mean 'recipientOrganizationId'?
|
||||
|
||||
93 originatorOrganizationId: dto.originatorOrganizationId,
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
src/modules/document-numbering/dto/preview-number.dto.ts:27:3
|
||||
27 recipientOrganizationId?: number;
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
'recipientOrganizationId' is declared here.
|
||||
src/modules/document-numbering/controllers/document-numbering.controller.ts:94:19 - error TS2339: Property 'correspondenceTypeId' does not exist on type 'PreviewNumberDto'.
|
||||
|
||||
94 typeId: dto.correspondenceTypeId,
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/controllers/document-numbering.controller.ts:100:25 - error TS2339: Property 'customTokens' does not exist on type 'PreviewNumberDto'.
|
||||
|
||||
100 customTokens: dto.customTokens,
|
||||
~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/confirm-reservation.dto.ts:13:3 - error TS2564: Property 'documentNumber' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
13 documentNumber: string;
|
||||
~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/confirm-reservation.dto.ts:14:3 - error TS2564: Property 'confirmedAt' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
14 confirmedAt: Date;
|
||||
~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:2:3 - error TS2564: Property 'projectId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
2 projectId: number;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:3:3 - error TS2564: Property 'originatorOrganizationId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
3 originatorOrganizationId: number;
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:4:3 - error TS2564: Property 'recipientOrganizationId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
4 recipientOrganizationId: number;
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:5:3 - error TS2564: Property 'correspondenceTypeId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
5 correspondenceTypeId: number;
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:6:3 - error TS2564: Property 'subTypeId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
6 subTypeId: number;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:7:3 - error TS2564: Property 'rfaTypeId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
7 rfaTypeId: number;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:8:3 - error TS2564: Property 'disciplineId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
8 disciplineId: number;
|
||||
~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/counter-key.dto.ts:9:3 - error TS2564: Property 'resetScope' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
9 resetScope: string;
|
||||
~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:5:3 - error TS2564: Property 'projectId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
5 projectId: number;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:8:3 - error TS2564: Property 'originatorOrganizationId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
8 originatorOrganizationId: number;
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:15:3 - error TS2564: Property 'correspondenceTypeId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
15 correspondenceTypeId: number;
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:35:3 - error TS2564: Property 'token' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
35 token: string;
|
||||
~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:36:3 - error TS2564: Property 'documentNumber' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
36 documentNumber: string;
|
||||
~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/dto/reserve-number.dto.ts:37:3 - error TS2564: Property 'expiresAt' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
37 expiresAt: Date;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:20:3 - error TS2564: Property 'id' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
20 id: number;
|
||||
~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:23:3 - error TS2564: Property 'projectId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
23 projectId: number;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:26:3 - error TS2564: Property 'correspondenceTypeId' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
26 correspondenceTypeId: number | null;
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:29:3 - error TS2564: Property 'formatTemplate' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
29 formatTemplate: string;
|
||||
~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:32:3 - error TS2564: Property 'description' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
32 description: string;
|
||||
~~~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:36:3 - error TS2564: Property 'resetSequenceYearly' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
36 resetSequenceYearly: boolean;
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:39:3 - error TS2564: Property 'createdAt' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
39 createdAt: Date;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:42:3 - error TS2564: Property 'updatedAt' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
42 updatedAt: Date;
|
||||
~~~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:47:3 - error TS2564: Property 'project' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
47 project: Project;
|
||||
~~~~~~~
|
||||
src/modules/document-numbering/entities/document-number-format.entity.ts:51:3 - error TS2564: Property 'correspondenceType' has no initializer and is not definitely assigned in the constructor.
|
||||
|
||||
51 correspondenceType: CorrespondenceType | null;
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
src/modules/document-numbering/services/document-numbering.service.ts:249:5 - error TS2740: Type 'DocumentNumberAudit[]' is missing the following properties from type 'DocumentNumberAudit': id, documentId, generatedNumber, counterKey, and 5 more.
|
||||
|
||||
249 return await this.auditRepo.save(audit);
|
||||
~~~~~~
|
||||
src/modules/document-numbering/services/document-numbering.service.ts:256:11 - error TS2769: No overload matches this call.
|
||||
Overload 1 of 3, '(entityLikeArray: DeepPartial<DocumentNumberError>[]): DocumentNumberError[]', gave the following error.
|
||||
Object literal may only specify known properties, and 'projectId' does not exist in type 'DeepPartial<DocumentNumberError>[]'.
|
||||
Overload 2 of 3, '(entityLike: DeepPartial<DocumentNumberError>): DocumentNumberError', gave the following error.
|
||||
Object literal may only specify known properties, and 'projectId' does not exist in type 'DeepPartial<DocumentNumberError>'.
|
||||
|
||||
256 projectId: ctx.projectId,
|
||||
~~~~~~~~~
|
||||
|
||||
|
||||
Found 31 error(s).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
services:
|
||||
mariadb_test:
|
||||
image: mariadb:11.8
|
||||
container_name: mariadb-test
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: Center#2025
|
||||
MYSQL_DATABASE: lcbp3_test
|
||||
MYSQL_USER: admin
|
||||
MYSQL_PASSWORD: Center2025
|
||||
ports:
|
||||
- '3307:3306'
|
||||
tmpfs:
|
||||
- /var/lib/mysql
|
||||
networks:
|
||||
- lcbp3-test-net
|
||||
|
||||
redis_test:
|
||||
image: redis:7-alpine
|
||||
container_name: redis-test
|
||||
restart: always
|
||||
command: redis-server --requirepass "Center2025"
|
||||
ports:
|
||||
- '6380:6379'
|
||||
networks:
|
||||
- lcbp3-test-net
|
||||
|
||||
networks:
|
||||
lcbp3-test-net:
|
||||
driver: bridge
|
||||
@@ -1,421 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
[Nest] 13440 - 12/09/2025, 8:34:55 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17)
|
||||
[Nest] 12240 - 12/09/2025, 8:34:55 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17)
|
||||
[Nest] 41780 - 12/09/2025, 8:34:55 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17)
|
||||
|
||||
ΓùÅ Cannot log after tests are done. Did you forget to wait for something async in your test?
|
||||
Attempted to log "AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
at console.error (../node_modules/@jest/console/build/index.js:124:10)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:129:17)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue.ts:192:18)
|
||||
at RedisConnection.<anonymous> (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:75:56)
|
||||
at EventEmitter.RedisConnection.handleClientError (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/redis-connection.ts:121:12)
|
||||
at EventEmitter.silentEmit (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/Redis.js:484:30)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/redis/event_handler.js:221:14)
|
||||
|
||||
FAIL test/app.e2e-spec.ts (7.608 s)
|
||||
ΓùÅ Console
|
||||
|
||||
console.error
|
||||
Redis Connection Error: AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
72 | imports: [ConfigModule],
|
||||
73 | useFactory: async (configService: ConfigService) => ({
|
||||
> 74 | store: await redisStore({
|
||||
| ^
|
||||
75 | socket: {
|
||||
76 | host: configService.get<string>('redis.host'),
|
||||
77 | port: configService.get<number>('redis.port'),
|
||||
|
||||
at redisStore (../../node_modules/.pnpm/cache-manager-redis-yet@5.1.5/node_modules/cache-manager-redis-yet/dist/index.js:101:17)
|
||||
at InstanceWrapper.useFactory [as metatype] (../src/app.module.ts:74:16)
|
||||
at TestingInjector.instantiateClass (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:424:37)
|
||||
at callback (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:70:34)
|
||||
at TestingInjector.resolveConstructorParams (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:170:24)
|
||||
at TestingInjector.loadInstance (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:75:13)
|
||||
at TestingInjector.loadProvider (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:103:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:56:13
|
||||
at async Promise.all (index 5)
|
||||
at TestingInstanceLoader.createInstancesOfProviders (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:55:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:40:13
|
||||
at async Promise.all (index 6)
|
||||
at TestingInstanceLoader.createInstances (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:39:9)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:22:13)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-instance-loader.js:9:9)
|
||||
at TestingModuleBuilder.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:118:9)
|
||||
at TestingModuleBuilder.compile (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:74:9)
|
||||
at Object.<anonymous> (app.e2e-spec.ts:11:42)
|
||||
|
||||
● AppController (e2e) › / (GET)
|
||||
|
||||
AggregateError:
|
||||
|
||||
|
||||
|
||||
ΓùÅ Cannot log after tests are done. Did you forget to wait for something async in your test?
|
||||
Attempted to log "AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
at console.error (../node_modules/@jest/console/build/index.js:124:10)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:129:17)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue.ts:192:18)
|
||||
at RedisConnection.<anonymous> (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:75:56)
|
||||
at EventEmitter.RedisConnection.handleClientError (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/redis-connection.ts:121:12)
|
||||
at EventEmitter.silentEmit (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/Redis.js:484:30)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/redis/event_handler.js:221:14)
|
||||
|
||||
FAIL test/simple.e2e-spec.ts (7.616 s)
|
||||
ΓùÅ Console
|
||||
|
||||
console.error
|
||||
Redis Connection Error: AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
72 | imports: [ConfigModule],
|
||||
73 | useFactory: async (configService: ConfigService) => ({
|
||||
> 74 | store: await redisStore({
|
||||
| ^
|
||||
75 | socket: {
|
||||
76 | host: configService.get<string>('redis.host'),
|
||||
77 | port: configService.get<number>('redis.port'),
|
||||
|
||||
at redisStore (../../node_modules/.pnpm/cache-manager-redis-yet@5.1.5/node_modules/cache-manager-redis-yet/dist/index.js:101:17)
|
||||
at InstanceWrapper.useFactory [as metatype] (../src/app.module.ts:74:16)
|
||||
at TestingInjector.instantiateClass (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:424:37)
|
||||
at callback (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:70:34)
|
||||
at TestingInjector.resolveConstructorParams (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:170:24)
|
||||
at TestingInjector.loadInstance (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:75:13)
|
||||
at TestingInjector.loadProvider (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:103:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:56:13
|
||||
at async Promise.all (index 5)
|
||||
at TestingInstanceLoader.createInstancesOfProviders (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:55:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:40:13
|
||||
at async Promise.all (index 6)
|
||||
at TestingInstanceLoader.createInstances (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:39:9)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:22:13)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-instance-loader.js:9:9)
|
||||
at TestingModuleBuilder.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:118:9)
|
||||
at TestingModuleBuilder.compile (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:74:9)
|
||||
at Object.<anonymous> (simple.e2e-spec.ts:9:42)
|
||||
|
||||
● Simple Test › should pass
|
||||
|
||||
AggregateError:
|
||||
|
||||
|
||||
|
||||
ΓùÅ Cannot log after tests are done. Did you forget to wait for something async in your test?
|
||||
Attempted to log "AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16)
|
||||
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at afterConnectMultiple (../node:net:1708:16) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at TCPConnectWrap.callbackTrampoline (../node:internal/async_hooks:130:17) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}".
|
||||
at console.error (../node_modules/@jest/console/build/index.js:124:10)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:129:17)
|
||||
at Queue.emit (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue.ts:192:18)
|
||||
at RedisConnection.<anonymous> (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/queue-base.ts:75:56)
|
||||
at EventEmitter.RedisConnection.handleClientError (../../node_modules/.pnpm/bullmq@5.65.0/node_modules/bullmq/src/classes/redis-connection.ts:121:12)
|
||||
at EventEmitter.silentEmit (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/Redis.js:484:30)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/ioredis@5.8.2/node_modules/ioredis/built/redis/event_handler.js:221:14)
|
||||
|
||||
FAIL test/phase3-workflow.e2e-spec.ts (7.637 s)
|
||||
ΓùÅ Console
|
||||
|
||||
console.error
|
||||
Redis Connection Error: AggregateError:
|
||||
at internalConnectMultiple (node:net:1134:18)
|
||||
at afterConnectMultiple (node:net:1715:7) {
|
||||
code: 'ECONNREFUSED',
|
||||
[errors]: [
|
||||
Error: connect ECONNREFUSED ::1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '::1',
|
||||
port: 6379
|
||||
},
|
||||
Error: connect ECONNREFUSED 127.0.0.1:6379
|
||||
at createConnectionError (node:net:1678:14)
|
||||
at afterConnectMultiple (node:net:1708:16) {
|
||||
errno: -4078,
|
||||
code: 'ECONNREFUSED',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
72 | imports: [ConfigModule],
|
||||
73 | useFactory: async (configService: ConfigService) => ({
|
||||
> 74 | store: await redisStore({
|
||||
| ^
|
||||
75 | socket: {
|
||||
76 | host: configService.get<string>('redis.host'),
|
||||
77 | port: configService.get<number>('redis.port'),
|
||||
|
||||
at redisStore (../../node_modules/.pnpm/cache-manager-redis-yet@5.1.5/node_modules/cache-manager-redis-yet/dist/index.js:101:17)
|
||||
at InstanceWrapper.useFactory [as metatype] (../src/app.module.ts:74:16)
|
||||
at TestingInjector.instantiateClass (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:424:37)
|
||||
at callback (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:70:34)
|
||||
at TestingInjector.resolveConstructorParams (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:170:24)
|
||||
at TestingInjector.loadInstance (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:75:13)
|
||||
at TestingInjector.loadProvider (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/injector.js:103:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:56:13
|
||||
at async Promise.all (index 5)
|
||||
at TestingInstanceLoader.createInstancesOfProviders (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:55:9)
|
||||
at ../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:40:13
|
||||
at async Promise.all (index 6)
|
||||
at TestingInstanceLoader.createInstances (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:39:9)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+core@11.1.9_@nestjs_89e063bd3a6d5071b082cab065bf34d7/node_modules/@nestjs/core/injector/instance-loader.js:22:13)
|
||||
at TestingInstanceLoader.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-instance-loader.js:9:9)
|
||||
at TestingModuleBuilder.createInstancesOfDependencies (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:118:9)
|
||||
at TestingModuleBuilder.compile (../../node_modules/.pnpm/@nestjs+testing@11.1.9_@nes_5fa0f54bf7d8c8acec998f5e81836857/node_modules/@nestjs/testing/testing-module.builder.js:74:9)
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:25:42)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
AggregateError:
|
||||
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit Workflow
|
||||
|
||||
AggregateError:
|
||||
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Approve Step
|
||||
|
||||
AggregateError:
|
||||
|
||||
|
||||
|
||||
ΓùÅ Test suite failed to run
|
||||
|
||||
TypeError: Cannot read properties of undefined (reading 'close')
|
||||
|
||||
70 | // Correspondence cleanup might be needed if not using a test DB
|
||||
71 | }
|
||||
> 72 | await app.close();
|
||||
| ^
|
||||
73 | });
|
||||
74 |
|
||||
75 | it('/correspondences (POST) - Create Document', async () => {
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:72:15)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 3 failed, 3 total
|
||||
Tests: 5 failed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.87 s
|
||||
Ran all test suites.
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 5332 - 12/09/2025, 11:25:20 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 5332 - 12/09/2025, 11:25:21 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0003-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
3,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0003-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 3, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0003-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 3, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
[Nest] 5332 - 12/09/2025, 11:25:21 AM ERROR [WorkflowEngineService] Transition Failed for c4765f7d-fb12-4ca8-9fa7-10a237069581: Cannot read properties of undefined (reading 'terminal')
|
||||
[Nest] 5332 - 12/09/2025, 11:25:21 AM ERROR [CorrespondenceWorkflowService] Failed to submit workflow: TypeError: Cannot read properties of undefined (reading 'terminal')
|
||||
[Nest] 5332 - 12/09/2025, 11:25:21 AM ERROR [ExceptionsHandler] TypeError: Cannot read properties of undefined (reading 'terminal')
|
||||
at WorkflowEngineService.processTransition (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.ts:274:36)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at CorrespondenceWorkflowService.submitWorkflow (D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence-workflow.service.ts:73:32)
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 5
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.321 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,100 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 16184 - 12/09/2025, 11:27:54 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 16184 - 12/09/2025, 11:27:54 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0004-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
4,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0004-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 4, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0004-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 4, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 6
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.log
|
||||
Workflow Instance ID: 3577a2e1-bada-4fe7-84f1-876ec83b0624
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:99:13)
|
||||
|
||||
console.log
|
||||
Current State: IN_REVIEW
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:100:13)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Process Action
|
||||
|
||||
expected 201 "Created", got 403 "Forbidden"
|
||||
|
||||
116 | comment: 'E2E Approved via Unified Workflow Engine',
|
||||
117 | })
|
||||
> 118 | .expect(201);
|
||||
| ^
|
||||
119 |
|
||||
120 | expect(response.body).toHaveProperty('success', true);
|
||||
121 | expect(response.body).toHaveProperty('nextState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:118:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.67 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,100 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 7212 - 12/09/2025, 11:32:17 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 7212 - 12/09/2025, 11:32:17 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0005-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
5,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0005-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 5, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0005-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 5, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 7
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.log
|
||||
Workflow Instance ID: 20c439a2-841c-40a1-96e7-5c9f8dfe234f
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:99:13)
|
||||
|
||||
console.log
|
||||
Current State: IN_REVIEW
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:100:13)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Process Action
|
||||
|
||||
expected 201 "Created", got 403 "Forbidden"
|
||||
|
||||
116 | comment: 'E2E Approved via Unified Workflow Engine',
|
||||
117 | })
|
||||
> 118 | .expect(201);
|
||||
| ^
|
||||
119 |
|
||||
120 | expect(response.body).toHaveProperty('success', true);
|
||||
121 | expect(response.body).toHaveProperty('nextState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:118:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.533 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,100 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 46180 - 12/09/2025, 11:40:20 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 46180 - 12/09/2025, 11:40:20 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0006-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
6,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0006-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 6, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0006-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 6, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 8
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.log
|
||||
Workflow Instance ID: 9fc9ddd7-5257-4363-b1f1-f9c22f581b44
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:99:13)
|
||||
|
||||
console.log
|
||||
Current State: IN_REVIEW
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:100:13)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Process Action
|
||||
|
||||
expected 201 "Created", got 403 "Forbidden"
|
||||
|
||||
116 | comment: 'E2E Approved via Unified Workflow Engine',
|
||||
117 | })
|
||||
> 118 | .expect(201);
|
||||
| ^
|
||||
119 |
|
||||
120 | expect(response.body).toHaveProperty('success', true);
|
||||
121 | expect(response.body).toHaveProperty('nextState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:118:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.568 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,84 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 38304 - 12/09/2025, 12:13:26 PM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 38304 - 12/09/2025, 12:13:26 PM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0007-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
7,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0007-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 7, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0007-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 7, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
PASS test/phase3-workflow.e2e-spec.ts (5.236 s)
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 9
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.log
|
||||
Workflow Instance ID: d601ef06-93e0-435c-ad76-fc6e3dee5c22
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:99:13)
|
||||
|
||||
console.log
|
||||
Current State: IN_REVIEW
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:100:13)
|
||||
|
||||
console.log
|
||||
Action Result: { success: true, nextState: 'APPROVED', events: [], isCompleted: true }
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:122:13)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
|
||||
Test Suites: 3 passed, 3 total
|
||||
Tests: 5 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 6.691 s
|
||||
Ran all test suites.
|
||||
@@ -1,84 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 38760 - 12/09/2025, 12:16:40 PM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 38760 - 12/09/2025, 12:16:40 PM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0008-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
8,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0008-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 8, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0008-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 8, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
PASS test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 10
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.log
|
||||
Workflow Instance ID: 5057da48-f0e5-4d1a-86f1-a1b96929a6eb
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:99:13)
|
||||
|
||||
console.log
|
||||
Current State: IN_REVIEW
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:100:13)
|
||||
|
||||
console.log
|
||||
Action Result: { success: true, nextState: 'APPROVED', events: [], isCompleted: true }
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:122:13)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
|
||||
Test Suites: 3 passed, 3 total
|
||||
Tests: 5 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.885 s, estimated 6 s
|
||||
Ran all test suites.
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts (7.275 s)
|
||||
PASS test/app.e2e-spec.ts (7.566 s)
|
||||
FAIL test/phase3-workflow.e2e-spec.ts (7.639 s)
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
QueryFailedError: Table 'lcbp3_dev.correspondence_routing_templates' doesn't exist
|
||||
|
||||
at Query.onResult (../../node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08/src/driver/mysql/MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:36:14)
|
||||
at PoolConnection.handlePacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34)
|
||||
at PacketParser.onPacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12)
|
||||
at PacketParser.executeStart (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit Workflow
|
||||
|
||||
QueryFailedError: Table 'lcbp3_dev.correspondence_routing_templates' doesn't exist
|
||||
|
||||
at Query.onResult (../../node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08/src/driver/mysql/MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:36:14)
|
||||
at PoolConnection.handlePacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34)
|
||||
at PacketParser.onPacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12)
|
||||
at PacketParser.executeStart (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Approve Step
|
||||
|
||||
QueryFailedError: Table 'lcbp3_dev.correspondence_routing_templates' doesn't exist
|
||||
|
||||
at Query.onResult (../../node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08/src/driver/mysql/MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:36:14)
|
||||
at PoolConnection.handlePacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34)
|
||||
at PacketParser.onPacket (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12)
|
||||
at PacketParser.executeStart (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (../../node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25)
|
||||
|
||||
|
||||
ΓùÅ Test suite failed to run
|
||||
|
||||
TypeORMError: Empty criteria(s) are not allowed for the delete method.
|
||||
|
||||
67 | if (dataSource) {
|
||||
68 | const templateRepo = dataSource.getRepository(RoutingTemplate);
|
||||
> 69 | await templateRepo.delete(templateId);
|
||||
| ^
|
||||
70 | // Correspondence cleanup might be needed if not using a test DB
|
||||
71 | }
|
||||
72 | await app.close();
|
||||
|
||||
at EntityManager.delete (../../node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08/src/entity-manager/EntityManager.ts:849:17)
|
||||
at Repository.delete (../../node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08/src/repository/Repository.ts:420:35)
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:69:32)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 3 failed, 2 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 9.08 s
|
||||
Ran all test suites.
|
||||
@@ -1,165 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
[Nest] 28712 - 12/09/2025, 9:48:43 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
[Nest] 40512 - 12/09/2025, 9:48:43 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
[Nest] 41884 - 12/09/2025, 9:48:43 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
[Nest] 41884 - 12/09/2025, 9:48:46 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (2)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
[Nest] 28712 - 12/09/2025, 9:48:46 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (2)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
[Nest] 40512 - 12/09/2025, 9:48:46 AM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (2)...
|
||||
TypeORMError: Entity metadata for RoutingTemplate#steps was not found. Check if you specified a correct entity object and if it's connected in the connection options.
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1128:23
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.computeInverseProperties (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:1118:34)
|
||||
at D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:160:18
|
||||
at Array.forEach (<anonymous>)
|
||||
at EntityMetadataBuilder.build (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\metadata-builder\EntityMetadataBuilder.ts:159:25)
|
||||
at ConnectionMetadataBuilder.buildEntityMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\connection\ConnectionMetadataBuilder.ts:106:11)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at DataSource.buildMetadatas (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:733:13)
|
||||
at DataSource.initialize (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\data-source\DataSource.ts:264:13)
|
||||
FAIL test/app.e2e-spec.ts (8.781 s)
|
||||
● AppController (e2e) › / (GET)
|
||||
|
||||
thrown: "Exceeded timeout of 5000 ms for a hook.
|
||||
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
|
||||
|
||||
8 | let app: INestApplication<App>;
|
||||
9 |
|
||||
> 10 | beforeEach(async () => {
|
||||
| ^
|
||||
11 | const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
12 | imports: [AppModule],
|
||||
13 | }).compile();
|
||||
|
||||
at app.e2e-spec.ts:10:3
|
||||
at Object.<anonymous> (app.e2e-spec.ts:7:1)
|
||||
|
||||
FAIL test/phase3-workflow.e2e-spec.ts (8.787 s)
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
thrown: "Exceeded timeout of 5000 ms for a hook.
|
||||
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
|
||||
|
||||
27 | let adminToken: string;
|
||||
28 |
|
||||
> 29 | beforeAll(async () => {
|
||||
| ^
|
||||
30 | const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
31 | imports: [AppModule],
|
||||
32 | }).compile();
|
||||
|
||||
at phase3-workflow.e2e-spec.ts:29:3
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:15:1)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
thrown: "Exceeded timeout of 5000 ms for a hook.
|
||||
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
|
||||
|
||||
27 | let adminToken: string;
|
||||
28 |
|
||||
> 29 | beforeAll(async () => {
|
||||
| ^
|
||||
30 | const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
31 | imports: [AppModule],
|
||||
32 | }).compile();
|
||||
|
||||
at phase3-workflow.e2e-spec.ts:29:3
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:15:1)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/workflow/action (POST) - Process Action
|
||||
|
||||
thrown: "Exceeded timeout of 5000 ms for a hook.
|
||||
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
|
||||
|
||||
27 | let adminToken: string;
|
||||
28 |
|
||||
> 29 | beforeAll(async () => {
|
||||
| ^
|
||||
30 | const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
31 | imports: [AppModule],
|
||||
32 | }).compile();
|
||||
|
||||
at phase3-workflow.e2e-spec.ts:29:3
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:15:1)
|
||||
|
||||
FAIL test/simple.e2e-spec.ts (8.797 s)
|
||||
● Simple Test › should pass
|
||||
|
||||
thrown: "Exceeded timeout of 5000 ms for a test.
|
||||
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
|
||||
|
||||
6 |
|
||||
7 | describe('Simple Test', () => {
|
||||
> 8 | it('should pass', async () => {
|
||||
| ^
|
||||
9 | const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
10 | imports: [AppModule],
|
||||
11 | }).compile();
|
||||
|
||||
at simple.e2e-spec.ts:8:3
|
||||
at Object.<anonymous> (simple.e2e-spec.ts:7:1)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 3 failed, 3 total
|
||||
Tests: 5 failed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 9.98 s
|
||||
Ran all test suites.
|
||||
@@ -1,83 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.warn
|
||||
WorkflowDefinition CORRESPONDENCE_FLOW_V1 not found. Tests may fail.
|
||||
|
||||
55 |
|
||||
56 | if (!existing) {
|
||||
> 57 | console.warn(
|
||||
| ^
|
||||
58 | 'WorkflowDefinition CORRESPONDENCE_FLOW_V1 not found. Tests may fail.'
|
||||
59 | );
|
||||
60 | }
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:57:15)
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
expected 201 "Created", got 403 "Forbidden"
|
||||
|
||||
77 | details: { question: 'Testing Unified Workflow' },
|
||||
78 | })
|
||||
> 79 | .expect(201);
|
||||
| ^
|
||||
80 |
|
||||
81 | expect(response.body).toHaveProperty('id');
|
||||
82 | expect(response.body).toHaveProperty('correspondenceNumber');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:79:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 403 "Forbidden"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 2 failed, 3 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.219 s, estimated 9 s
|
||||
Ran all test suites.
|
||||
@@ -1,214 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [DocumentNumberingService] Error generating number for doc_num:1:1:0:2025
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [DocumentNumberingService] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, 1) RETURNING `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
1,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, 1, 0, 2025, 1, 1) RETURNING `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, 1, 0, 2025, 1, 1) RETURNING `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [DocumentNumberingService] Failed to log error
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'error_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, DEFAULT) RETURNING `id`, `error_at`',
|
||||
parameters: [
|
||||
'doc_num:1:1:0:2025',
|
||||
'DB_ERROR',
|
||||
'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\n at Query.onResult (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\src\\driver\\mysql\\MysqlQueryRunner.ts:248:33)\n at Query.execute (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\commands\\command.js:36:14)\n at PoolConnection.handlePacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:477:34)\n at PacketParser.onPacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:93:12)\n at PacketParser.executeStart (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\packet_parser.js:75:16)\n at Socket.<anonymous> (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:100:25)\n at Socket.emit (node:events:519:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Socket.Readable.push (node:internal/streams/readable:392:5)\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)',
|
||||
'{"projectId":1,"originatorId":41,"typeId":1,"year":2025,"customTokens":{"TYPE_CODE":"RFA","ORG_CODE":"ORG"}}'
|
||||
],
|
||||
driverError: Error: Unknown column 'error_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
}
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [CorrespondenceService] Failed to create correspondence: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
[Nest] 45012 - 12/09/2025, 10:04:29 AM ERROR [ExceptionsHandler] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, 1) RETURNING `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
1,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, 1, 0, 2025, 1, 1) RETURNING `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `correspondence_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, 1, 0, 2025, 1, 1) RETURNING `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
77 | details: { question: 'Testing Unified Workflow' },
|
||||
78 | })
|
||||
> 79 | .expect(201);
|
||||
| ^
|
||||
80 |
|
||||
81 | expect(response.body).toHaveProperty('id');
|
||||
82 | expect(response.body).toHaveProperty('correspondenceNumber');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:79:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 400 "Bad Request"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 2 failed, 3 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.122 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,220 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts (7.012 s)
|
||||
PASS test/app.e2e-spec.ts (7.175 s)
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [DocumentNumberingService] Error generating number for doc_num:1:1:0:2025
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [DocumentNumberingService] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
-1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [DocumentNumberingService] Failed to log error
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'error_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, DEFAULT) RETURNING `id`, `error_at`',
|
||||
parameters: [
|
||||
'doc_num:1:1:0:2025',
|
||||
'DB_ERROR',
|
||||
'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\n at Query.onResult (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\src\\driver\\mysql\\MysqlQueryRunner.ts:248:33)\n at Query.execute (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\commands\\command.js:36:14)\n at PoolConnection.handlePacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:477:34)\n at PacketParser.onPacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:93:12)\n at PacketParser.executeStart (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\packet_parser.js:75:16)\n at Socket.<anonymous> (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:100:25)\n at Socket.emit (node:events:519:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Socket.Readable.push (node:internal/streams/readable:392:5)\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)',
|
||||
'{"projectId":1,"originatorId":41,"typeId":1,"year":2025,"customTokens":{"TYPE_CODE":"RFA","ORG_CODE":"ORG"}}'
|
||||
],
|
||||
driverError: Error: Unknown column 'error_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
}
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [CorrespondenceService] Failed to create correspondence: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
[Nest] 22264 - 12/09/2025, 10:27:45 AM ERROR [ExceptionsHandler] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
-1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `document_number_counters_ibfk_3` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts (7.412 s)
|
||||
ΓùÅ Console
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
77 | details: { question: 'Testing Unified Workflow' },
|
||||
78 | })
|
||||
> 79 | .expect(201);
|
||||
| ^
|
||||
80 |
|
||||
81 | expect(response.body).toHaveProperty('id');
|
||||
82 | expect(response.body).toHaveProperty('correspondenceNumber');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:79:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 400 "Bad Request"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 2 failed, 3 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 8.723 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,220 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [DocumentNumberingService] Error generating number for doc_num:1:1:0:2025
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [DocumentNumberingService] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
-1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [DocumentNumberingService] Failed to log error
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'error_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, DEFAULT) RETURNING `id`, `error_at`',
|
||||
parameters: [
|
||||
'doc_num:1:1:0:2025',
|
||||
'DB_ERROR',
|
||||
'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\n at Query.onResult (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\src\\driver\\mysql\\MysqlQueryRunner.ts:248:33)\n at Query.execute (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\commands\\command.js:36:14)\n at PoolConnection.handlePacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:477:34)\n at PacketParser.onPacket (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:93:12)\n at PacketParser.executeStart (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\packet_parser.js:75:16)\n at Socket.<anonymous> (D:\\nap-dms.lcbp3\\node_modules\\.pnpm\\mysql2@3.15.3\\node_modules\\mysql2\\lib\\base\\connection.js:100:25)\n at Socket.emit (node:events:519:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Socket.Readable.push (node:internal/streams/readable:392:5)\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)',
|
||||
'{"projectId":1,"originatorId":41,"typeId":1,"year":2025,"customTokens":{"TYPE_CODE":"RFA","ORG_CODE":"ORG"}}'
|
||||
],
|
||||
driverError: Error: Unknown column 'error_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'error_at' in 'RETURNING'",
|
||||
sql: 'INSERT INTO `document_number_errors`(`id`, `counter_key`, `error_type`, `error_message`, `stack_trace`, `user_id`, `ip_address`, `context`, `error_at`) VALUES (DEFAULT, \'doc_num:1:1:0:2025\', \'DB_ERROR\', \'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\', \'QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)\\n at Query.onResult (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\\\\src\\\\driver\\\\mysql\\\\MysqlQueryRunner.ts:248:33)\\n at Query.execute (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\commands\\\\command.js:36:14)\\n at PoolConnection.handlePacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:477:34)\\n at PacketParser.onPacket (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:93:12)\\n at PacketParser.executeStart (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\packet_parser.js:75:16)\\n at Socket.<anonymous> (D:\\\\nap-dms.lcbp3\\\\node_modules\\\\.pnpm\\\\mysql2@3.15.3\\\\node_modules\\\\mysql2\\\\lib\\\\base\\\\connection.js:100:25)\\n at Socket.emit (node:events:519:28)\\n at addChunk (node:internal/streams/readable:561:12)\\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\\n at Socket.Readable.push (node:internal/streams/readable:392:5)\\n at TCP.onStreamRead (node:internal/stream_base_commons:189:23)\\n at TCP.callbackTrampoline (node:internal/async_hooks:130:17)\', DEFAULT, DEFAULT, \'{\\"projectId\\":1,\\"originatorId\\":41,\\"typeId\\":1,\\"year\\":2025,\\"customTokens\\":{\\"TYPE_CODE\\":\\"RFA\\",\\"ORG_CODE\\":\\"ORG\\"}}\', DEFAULT) RETURNING `id`, `error_at`'
|
||||
}
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [CorrespondenceService] Failed to create correspondence: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
[Nest] 44520 - 12/09/2025, 11:16:08 AM ERROR [ExceptionsHandler] QueryFailedError: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`',
|
||||
parameters: [
|
||||
1,
|
||||
41,
|
||||
-1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2025,
|
||||
1
|
||||
],
|
||||
driverError: Error: Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
},
|
||||
code: 'ER_NO_REFERENCED_ROW_2',
|
||||
errno: 1452,
|
||||
sqlState: '23000',
|
||||
sqlMessage: 'Cannot add or update a child row: a foreign key constraint fails (`lcbp3_dev`.`document_number_counters`, CONSTRAINT `fk_recipient_when_not_all` FOREIGN KEY (`recipient_organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE)',
|
||||
sql: 'INSERT INTO `document_number_counters`(`project_id`, `originator_organization_id`, `recipient_organization_id`, `correspondence_type_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `current_year`, `last_number`, `version`) VALUES (1, 41, -1, 1, 0, 0, 0, 2025, 1, 1) RETURNING `recipient_organization_id`, `sub_type_id`, `rfa_type_id`, `discipline_id`, `last_number`, `version`'
|
||||
}
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences (POST) - Create Document
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
77 | details: { question: 'Testing Unified Workflow' },
|
||||
78 | })
|
||||
> 79 | .expect(201);
|
||||
| ^
|
||||
80 |
|
||||
81 | expect(response.body).toHaveProperty('id');
|
||||
82 | expect(response.body).toHaveProperty('correspondenceNumber');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:79:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 400 "Bad Request"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 2 failed, 3 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.786 s, estimated 8 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,111 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 25968 - 12/09/2025, 11:19:28 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 25968 - 12/09/2025, 11:19:28 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0001-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0001-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 1, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0001-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 1, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
[Nest] 25968 - 12/09/2025, 11:19:28 AM ERROR [WorkflowEngineService] Transition Failed for 1215d0aa-453f-46dc-845d-0488a0213c4a: Cannot read properties of undefined (reading 'roles')
|
||||
[Nest] 25968 - 12/09/2025, 11:19:28 AM ERROR [CorrespondenceWorkflowService] Failed to submit workflow: TypeError: Cannot read properties of undefined (reading 'roles')
|
||||
[Nest] 25968 - 12/09/2025, 11:19:28 AM ERROR [ExceptionsHandler] TypeError: Cannot read properties of undefined (reading 'roles')
|
||||
at WorkflowDslService.checkRequirements (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-dsl.service.ts:219:13)
|
||||
at WorkflowDslService.evaluate (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-dsl.service.ts:178:10)
|
||||
at WorkflowEngineService.processTransition (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.ts:259:42)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at CorrespondenceWorkflowService.submitWorkflow (D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence-workflow.service.ts:72:32)
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 3
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.439 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,111 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test:e2e D:\nap-dms.lcbp3\backend
|
||||
> jest --config ./test/jest-e2e.json
|
||||
|
||||
PASS test/simple.e2e-spec.ts
|
||||
PASS test/app.e2e-spec.ts
|
||||
[Nest] 35280 - 12/09/2025, 11:24:24 AM ERROR [DocumentNumberingService] Failed to log audit
|
||||
[Nest] 35280 - 12/09/2025, 11:24:24 AM ERROR [DocumentNumberingService] QueryFailedError: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Query.onResult (D:\nap-dms.lcbp3\node_modules\.pnpm\typeorm@0.3.27_ioredis@5.8._cb81dfd56f1203fe00eb0fec5dfcce08\src\driver\mysql\MysqlQueryRunner.ts:248:33)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:36:14)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
query: 'INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT, DEFAULT, ?, ?, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`',
|
||||
parameters: [
|
||||
'ผรม.1-ผรม.1-0002-2568',
|
||||
'doc_num:1:1:0:2025',
|
||||
'{ORG}-{ORG}-{SEQ:4}-{YEAR}',
|
||||
2,
|
||||
0,
|
||||
0
|
||||
],
|
||||
driverError: Error: Unknown column 'generated_at' in 'RETURNING'
|
||||
at Packet.asError (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packets\packet.js:740:17)
|
||||
at Query.execute (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\commands\command.js:29:26)
|
||||
at PoolConnection.handlePacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:477:34)
|
||||
at PacketParser.onPacket (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:93:12)
|
||||
at PacketParser.executeStart (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\packet_parser.js:75:16)
|
||||
at Socket.<anonymous> (D:\nap-dms.lcbp3\node_modules\.pnpm\mysql2@3.15.3\node_modules\mysql2\lib\base\connection.js:100:25)
|
||||
at Socket.emit (node:events:519:28)
|
||||
at addChunk (node:internal/streams/readable:561:12)
|
||||
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
|
||||
at Socket.Readable.push (node:internal/streams/readable:392:5)
|
||||
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
|
||||
at TCP.callbackTrampoline (node:internal/async_hooks:130:17) {
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0002-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 2, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
},
|
||||
code: 'ER_BAD_FIELD_ERROR',
|
||||
errno: 1054,
|
||||
sqlState: '42S22',
|
||||
sqlMessage: "Unknown column 'generated_at' in 'RETURNING'",
|
||||
sql: "INSERT INTO `document_number_audit`(`id`, `generated_number`, `counter_key`, `template_used`, `sequence_number`, `user_id`, `ip_address`, `retry_count`, `lock_wait_ms`, `generated_at`) VALUES (DEFAULT, 'ผรม.1-ผรม.1-0002-2568', 'doc_num:1:1:0:2025', '{ORG}-{ORG}-{SEQ:4}-{YEAR}', 2, DEFAULT, DEFAULT, 0, 0, DEFAULT) RETURNING `id`, `retry_count`, `generated_at`"
|
||||
}
|
||||
[Nest] 35280 - 12/09/2025, 11:24:25 AM ERROR [WorkflowEngineService] Transition Failed for 3a51f630-c4fc-4fb4-8c2b-f1150195d8bd: Cannot read properties of undefined (reading 'roles')
|
||||
[Nest] 35280 - 12/09/2025, 11:24:25 AM ERROR [CorrespondenceWorkflowService] Failed to submit workflow: TypeError: Cannot read properties of undefined (reading 'roles')
|
||||
[Nest] 35280 - 12/09/2025, 11:24:25 AM ERROR [ExceptionsHandler] TypeError: Cannot read properties of undefined (reading 'roles')
|
||||
at WorkflowDslService.checkRequirements (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-dsl.service.ts:219:13)
|
||||
at WorkflowDslService.evaluate (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-dsl.service.ts:178:10)
|
||||
at WorkflowEngineService.processTransition (D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.ts:259:42)
|
||||
at processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at CorrespondenceWorkflowService.submitWorkflow (D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence-workflow.service.ts:73:32)
|
||||
FAIL test/phase3-workflow.e2e-spec.ts
|
||||
ΓùÅ Console
|
||||
|
||||
console.log
|
||||
Created Correspondence ID: 4
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:84:13)
|
||||
|
||||
console.warn
|
||||
Skipping action test - no instanceId from submit
|
||||
|
||||
104 | // Skip if submit failed to get instanceId
|
||||
105 | if (!workflowInstanceId) {
|
||||
> 106 | console.warn('Skipping action test - no instanceId from submit');
|
||||
| ^
|
||||
107 | return;
|
||||
108 | }
|
||||
109 |
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:106:15)
|
||||
|
||||
● Phase 3 Workflow (E2E) › /correspondences/:id/submit (POST) - Submit to Workflow
|
||||
|
||||
expected 201 "Created", got 500 "Internal Server Error"
|
||||
|
||||
92 | note: 'Submitting for E2E test',
|
||||
93 | })
|
||||
> 94 | .expect(201);
|
||||
| ^
|
||||
95 |
|
||||
96 | expect(response.body).toHaveProperty('instanceId');
|
||||
97 | expect(response.body).toHaveProperty('currentState');
|
||||
|
||||
at Object.<anonymous> (phase3-workflow.e2e-spec.ts:94:8)
|
||||
----
|
||||
at Test._assertStatus (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:309:14)
|
||||
at ../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:365:13
|
||||
at Test._assertFunction (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:342:13)
|
||||
at Test.assert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:195:23)
|
||||
at localAssert (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:138:14)
|
||||
at Server.<anonymous> (../../node_modules/.pnpm/supertest@7.1.4/node_modules/supertest/lib/test.js:152:11)
|
||||
|
||||
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
||||
Test Suites: 1 failed, 2 passed, 3 total
|
||||
Tests: 1 failed, 4 passed, 5 total
|
||||
Snapshots: 0 total
|
||||
Time: 5.652 s
|
||||
Ran all test suites.
|
||||
ΓÇëELIFECYCLEΓÇë Command failed with exit code 1.
|
||||
@@ -1,105 +0,0 @@
|
||||
-- Migration: Align Schema with Documentation
|
||||
-- Version: 1733800000000
|
||||
-- Date: 2025-12-10
|
||||
-- Description: Add missing fields and fix column lengths to match schema v1.5.1
|
||||
-- ==========================================================
|
||||
-- Phase 1: Organizations Table Updates
|
||||
-- ==========================================================
|
||||
-- Add role_id column to organizations
|
||||
ALTER TABLE organizations
|
||||
ADD COLUMN role_id INT NULL COMMENT 'Reference to organization_roles table';
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE organizations
|
||||
ADD CONSTRAINT fk_organizations_role FOREIGN KEY (role_id) REFERENCES organization_roles(id) ON DELETE
|
||||
SET NULL;
|
||||
|
||||
-- Modify organization_name length from 200 to 255
|
||||
ALTER TABLE organizations
|
||||
MODIFY COLUMN organization_name VARCHAR(255) NOT NULL COMMENT 'Organization name';
|
||||
|
||||
-- ==========================================================
|
||||
-- Phase 2: Users Table Updates (Security Fields)
|
||||
-- ==========================================================
|
||||
-- Add failed_attempts for login tracking
|
||||
ALTER TABLE users
|
||||
ADD COLUMN failed_attempts INT DEFAULT 0 COMMENT 'Number of failed login attempts';
|
||||
|
||||
-- Add locked_until for account lockout mechanism
|
||||
ALTER TABLE users
|
||||
ADD COLUMN locked_until DATETIME NULL COMMENT 'Account locked until this timestamp';
|
||||
|
||||
-- Add last_login_at for audit trail
|
||||
ALTER TABLE users
|
||||
ADD COLUMN last_login_at TIMESTAMP NULL COMMENT 'Last successful login timestamp';
|
||||
|
||||
-- ==========================================================
|
||||
-- Phase 3: Roles Table Updates
|
||||
-- ==========================================================
|
||||
-- Modify role_name length from 50 to 100
|
||||
ALTER TABLE roles
|
||||
MODIFY COLUMN role_name VARCHAR(100) NOT NULL COMMENT 'Role name';
|
||||
|
||||
-- ==========================================================
|
||||
-- Verification Queries
|
||||
-- ==========================================================
|
||||
-- Verify organizations table structure
|
||||
SELECT COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
CHARACTER_MAXIMUM_LENGTH,
|
||||
IS_NULLABLE,
|
||||
COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'organizations'
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
-- Verify users table has new security fields
|
||||
SELECT COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
COLUMN_DEFAULT,
|
||||
IS_NULLABLE,
|
||||
COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME IN (
|
||||
'failed_attempts',
|
||||
'locked_until',
|
||||
'last_login_at'
|
||||
)
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
-- Verify roles table role_name length
|
||||
SELECT COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
CHARACTER_MAXIMUM_LENGTH
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'roles'
|
||||
AND COLUMN_NAME = 'role_name';
|
||||
|
||||
-- ==========================================================
|
||||
-- Rollback Script (Use if needed)
|
||||
-- ==========================================================
|
||||
/*
|
||||
-- Rollback Phase 3: Roles
|
||||
ALTER TABLE roles
|
||||
MODIFY COLUMN role_name VARCHAR(50) NOT NULL;
|
||||
|
||||
-- Rollback Phase 2: Users
|
||||
ALTER TABLE users
|
||||
DROP COLUMN last_login_at,
|
||||
DROP COLUMN locked_until,
|
||||
DROP COLUMN failed_attempts;
|
||||
|
||||
-- Rollback Phase 1: Organizations
|
||||
ALTER TABLE organizations
|
||||
MODIFY COLUMN organization_name VARCHAR(200) NOT NULL;
|
||||
|
||||
ALTER TABLE organizations
|
||||
DROP FOREIGN KEY fk_organizations_role;
|
||||
|
||||
ALTER TABLE organizations
|
||||
DROP COLUMN role_id;
|
||||
*/
|
||||
@@ -7,15 +7,13 @@
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"doc": "npx @compodoc/compodoc -p tsconfig.doc.json -s",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest --forceExit",
|
||||
"test:debug-handles": "jest --detectOpenHandles",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
@@ -45,7 +43,6 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.1.9",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@willsoto/nestjs-prometheus": "^6.0.2",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"async-retry": "^1.3.3",
|
||||
@@ -74,12 +71,11 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.27",
|
||||
"uuid": "^9.0.1",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.18.3",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@compodoc/compodoc": "^1.1.32",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
@@ -97,7 +93,7 @@
|
||||
"@types/opossum": "^8.1.9",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
9656
backend/pnpm-lock.yaml
generated
Normal file
9656
backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
|
||||
> backend@1.5.1 test D:\nap-dms.lcbp3\backend
|
||||
> jest --forceExit "rfa"
|
||||
|
||||
No tests found, exiting with code 1
|
||||
Run with `--passWithNoTests` to exit with code 0
|
||||
In D:\nap-dms.lcbp3\backend\src
|
||||
273 files checked.
|
||||
testMatch: - 0 matches
|
||||
testPathIgnorePatterns: \\node_modules\\ - 273 matches
|
||||
testRegex: .*\.spec\.ts$ - 15 matches
|
||||
Pattern: rfa - 0 matches
|
||||
ΓÇëELIFECYCLEΓÇë Test failed. See above for more details.
|
||||
@@ -1,31 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { databaseConfig } from '../src/config/database.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function checkConnection() {
|
||||
console.log('Checking database connection...');
|
||||
console.log(`Host: ${process.env.DB_HOST}`);
|
||||
console.log(`Port: ${process.env.DB_PORT}`);
|
||||
console.log(`User: ${process.env.DB_USERNAME}`);
|
||||
console.log(`Database: ${process.env.DB_DATABASE}`);
|
||||
|
||||
const dataSource = new DataSource(databaseConfig as MysqlConnectionOptions);
|
||||
|
||||
try {
|
||||
await dataSource.initialize();
|
||||
console.log('✅ Connection initialized successfully!');
|
||||
|
||||
const result = await dataSource.query('SHOW COLUMNS FROM rfa_types');
|
||||
console.log('rfa_types columns:', result);
|
||||
|
||||
await dataSource.destroy();
|
||||
} catch (error) {
|
||||
console.error('❌ Connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkConnection();
|
||||
@@ -29,8 +29,6 @@ import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
||||
import { AuthModule } from './common/auth/auth.module.js';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import { ProjectModule } from './modules/project/project.module';
|
||||
import { OrganizationModule } from './modules/organization/organization.module';
|
||||
import { ContractModule } from './modules/contract/contract.module';
|
||||
import { MasterModule } from './modules/master/master.module'; // [NEW] ✅ เพิ่ม MasterModule
|
||||
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
||||
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
||||
@@ -42,11 +40,9 @@ import { DrawingModule } from './modules/drawing/drawing.module';
|
||||
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
||||
import { CirculationModule } from './modules/circulation/circulation.module';
|
||||
import { NotificationModule } from './modules/notification/notification.module';
|
||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
||||
import { ResilienceModule } from './common/resilience/resilience.module';
|
||||
import { SearchModule } from './modules/search/search.module';
|
||||
import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -92,7 +88,7 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
@@ -111,7 +107,7 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
@@ -140,10 +136,7 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
// 📦 Feature Modules
|
||||
AuthModule,
|
||||
UserModule,
|
||||
UserModule,
|
||||
ProjectModule,
|
||||
OrganizationModule,
|
||||
ContractModule,
|
||||
MasterModule, // ✅ [NEW] Register MasterModule here
|
||||
FileStorageModule,
|
||||
DocumentNumberingModule,
|
||||
@@ -156,8 +149,6 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
CirculationModule,
|
||||
SearchModule,
|
||||
NotificationModule,
|
||||
DashboardModule,
|
||||
AuditLogModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -1,86 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
|
||||
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockAuthService = {
|
||||
validateUser: jest.fn(),
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
};
|
||||
@Post('login')
|
||||
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: mockAuthService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
});
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return tokens when credentials are valid', async () => {
|
||||
const loginDto = { username: 'test', password: 'password' };
|
||||
const mockUser = { user_id: 1, username: 'test' };
|
||||
const mockTokens = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
user: mockUser,
|
||||
};
|
||||
|
||||
(mockAuthService.validateUser as jest.Mock).mockResolvedValue(mockUser);
|
||||
(mockAuthService.login as jest.Mock).mockResolvedValue(mockTokens);
|
||||
|
||||
const result = await controller.login(loginDto);
|
||||
|
||||
expect(mockAuthService.validateUser).toHaveBeenCalledWith(
|
||||
'test',
|
||||
'password'
|
||||
);
|
||||
expect(mockAuthService.login).toHaveBeenCalledWith(mockUser);
|
||||
expect(result).toEqual(mockTokens);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException when credentials are invalid', async () => {
|
||||
const loginDto = { username: 'test', password: 'wrong' };
|
||||
(mockAuthService.validateUser as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
await expect(controller.login(loginDto)).rejects.toThrow(
|
||||
UnauthorizedException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
const registerDto = {
|
||||
username: 'newuser',
|
||||
password: 'password',
|
||||
email: 'test@test.com',
|
||||
display_name: 'Test User',
|
||||
};
|
||||
const mockUser = { user_id: 1, ...registerDto };
|
||||
|
||||
(mockAuthService.register as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const result = await controller.register(registerDto);
|
||||
|
||||
expect(mockAuthService.register).toHaveBeenCalledWith(registerDto);
|
||||
});
|
||||
});
|
||||
});
|
||||
@Post('register-admin')
|
||||
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,25 +11,17 @@ import {
|
||||
Req,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiResponse,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js';
|
||||
import { RegisterDto } from './dto/register.dto.js';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard.js';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Request } from 'express'; // ✅ Import Request
|
||||
|
||||
// สร้าง Interface สำหรับ Request ที่มี User
|
||||
// สร้าง Interface สำหรับ Request ที่มี User (เพื่อให้ TS รู้จัก req.user)
|
||||
interface RequestWithUser extends Request {
|
||||
user: any;
|
||||
}
|
||||
@@ -42,24 +34,11 @@ export class AuthController {
|
||||
@Post('login')
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Login to get Access & Refresh Token' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Login successful',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
access_token: { type: 'string' },
|
||||
refresh_token: { type: 'string' },
|
||||
user: { type: 'object' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' })
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password
|
||||
loginDto.password,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
@@ -72,9 +51,7 @@ export class AuthController {
|
||||
@Post('register-admin')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create new user (Admin Only)' })
|
||||
@ApiBody({ type: RegisterDto })
|
||||
@ApiResponse({ status: 201, description: 'User registered' })
|
||||
@ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' })
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
@@ -82,20 +59,9 @@ export class AuthController {
|
||||
@UseGuards(JwtRefreshGuard)
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Refresh Access Token using Refresh Token' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Token refreshed',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
access_token: { type: 'string' },
|
||||
refresh_token: { type: 'string' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' })
|
||||
async refresh(@Req() req: RequestWithUser) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
||||
}
|
||||
|
||||
@@ -103,51 +69,23 @@ export class AuthController {
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Logout (Revoke Tokens)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Logged out successfully',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string', example: 'Logged out successfully' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' })
|
||||
async logout(@Req() req: RequestWithUser) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
// ต้องเช็คว่ามี token หรือไม่ เพื่อป้องกัน runtime error
|
||||
if (!token) {
|
||||
return { message: 'No token provided' };
|
||||
}
|
||||
// ส่ง refresh token ไปด้วยถ้ามี (ใน header หรือ body)
|
||||
// สำหรับตอนนี้ส่งแค่ access token ไป blacklist
|
||||
return this.authService.logout(req.user.sub, token);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('profile')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get current user profile' })
|
||||
@ApiResponse({ status: 200, description: 'User profile' })
|
||||
@ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' })
|
||||
getProfile(@Req() req: RequestWithUser) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
return req.user;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('sessions')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get active sessions' })
|
||||
@ApiResponse({ status: 200, description: 'List of active sessions' })
|
||||
async getSessions() {
|
||||
return this.authService.getActiveSessions();
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('sessions/:id')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Revoke session' })
|
||||
@ApiResponse({ status: 200, description: 'Session revoked' })
|
||||
async revokeSession(@Param('id') id: string) {
|
||||
return this.authService.revokeSession(parseInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// File: src/common/auth/auth.module.ts
|
||||
// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322)
|
||||
// [P0-1] เพิ่ม CASL RBAC Integration
|
||||
// [P2-2] Register RefreshToken Entity
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@@ -14,19 +13,18 @@ import { UserModule } from '../../modules/user/user.module.js';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy.js';
|
||||
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||
import { CaslModule } from './casl/casl.module';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
import { CaslModule } from './casl/casl.module'; // [P0-1] Import CASL
|
||||
import { PermissionsGuard } from './guards/permissions.guard'; // [P0-1] Import Guard
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, RefreshToken]), // [P2-2] Added RefreshToken
|
||||
TypeOrmModule.forFeature([User]),
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: (configService.get<string>('JWT_EXPIRATION') ||
|
||||
@@ -34,10 +32,18 @@ import { PermissionsGuard } from './guards/permissions.guard';
|
||||
},
|
||||
}),
|
||||
}),
|
||||
CaslModule,
|
||||
CaslModule, // [P0-1] Import CASL module
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
JwtRefreshStrategy,
|
||||
PermissionsGuard, // [P0-1] Register PermissionsGuard
|
||||
],
|
||||
providers: [AuthService, JwtStrategy, JwtRefreshStrategy, PermissionsGuard],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService, PermissionsGuard],
|
||||
exports: [
|
||||
AuthService,
|
||||
PermissionsGuard, // [P0-1] Export for use in other modules
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,201 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
// Mock bcrypt at top level
|
||||
jest.mock('bcrypt', () => ({
|
||||
compare: jest.fn(),
|
||||
hash: jest.fn().mockResolvedValue('hashedpassword'),
|
||||
genSalt: jest.fn().mockResolvedValue('salt'),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
let userService: UserService;
|
||||
let jwtService: JwtService;
|
||||
let tokenRepo: Repository<RefreshToken>;
|
||||
|
||||
const mockUser = {
|
||||
user_id: 1,
|
||||
username: 'testuser',
|
||||
password: 'hashedpassword',
|
||||
primaryOrganizationId: 1,
|
||||
};
|
||||
|
||||
const mockQueryBuilder = {
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue(mockUser),
|
||||
};
|
||||
|
||||
const mockUserRepo = {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
};
|
||||
|
||||
const mockTokenRepo = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset bcrypt mocks
|
||||
bcrypt.compare.mockResolvedValue(true);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthService,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
findOneByUsername: jest.fn(),
|
||||
create: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: JwtService,
|
||||
useValue: {
|
||||
signAsync: jest.fn().mockResolvedValue('jwt_token'),
|
||||
decode: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn().mockImplementation((key: string) => {
|
||||
if (key.includes('EXPIRATION')) return '1h';
|
||||
return 'secret';
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockUserRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RefreshToken),
|
||||
useValue: mockTokenRepo,
|
||||
},
|
||||
],
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
jwtService = module.get<JwtService>(JwtService);
|
||||
tokenRepo = module.get(getRepositoryToken(RefreshToken));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('validateUser', () => {
|
||||
it('should return user without password if validation succeeds', async () => {
|
||||
const result = await service.validateUser('testuser', 'password');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toHaveProperty('password');
|
||||
expect(result.username).toBe('testuser');
|
||||
});
|
||||
|
||||
it('should return null if user not found', async () => {
|
||||
mockQueryBuilder.getOne.mockResolvedValueOnce(null);
|
||||
const result = await service.validateUser('unknown', 'password');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if password mismatch', async () => {
|
||||
bcrypt.compare.mockResolvedValueOnce(false);
|
||||
const result = await service.validateUser('testuser', 'wrongpassword');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return access and refresh tokens', async () => {
|
||||
mockTokenRepo.create.mockReturnValue({ id: 1 });
|
||||
mockTokenRepo.save.mockResolvedValue({ id: 1 });
|
||||
|
||||
const result = await service.login(mockUser);
|
||||
|
||||
expect(result).toHaveProperty('access_token');
|
||||
expect(result).toHaveProperty('refresh_token');
|
||||
expect(mockTokenRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
(userService.findOneByUsername as jest.Mock).mockResolvedValue(null);
|
||||
(userService.create as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const dto = {
|
||||
username: 'newuser',
|
||||
password: 'password',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
};
|
||||
|
||||
const result = await service.register(dto);
|
||||
expect(result).toBeDefined();
|
||||
expect(userService.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should return new tokens if valid', async () => {
|
||||
const mockStoredToken = {
|
||||
tokenHash: 'somehash',
|
||||
isRevoked: false,
|
||||
expiresAt: new Date(Date.now() + 10000),
|
||||
};
|
||||
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
|
||||
(userService.findOne as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.refreshToken(1, 'valid_refresh_token');
|
||||
|
||||
expect(result.access_token).toBeDefined();
|
||||
expect(result.refresh_token).toBeDefined();
|
||||
// Should mark old token as revoked
|
||||
expect(mockTokenRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isRevoked: true })
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException if token revoked', async () => {
|
||||
const mockStoredToken = {
|
||||
tokenHash: 'somehash',
|
||||
isRevoked: true,
|
||||
expiresAt: new Date(Date.now() + 10000),
|
||||
};
|
||||
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
|
||||
|
||||
await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(
|
||||
UnauthorizedException
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// บันทึกการแก้ไข:
|
||||
// 1. แก้ไข Type Mismatch ใน signAsync
|
||||
// 2. แก้ไข validateUser ให้ดึง password_hash ออกมาด้วย (Fix HTTP 500: data and hash arguments required)
|
||||
// 3. [P2-2] Implement Refresh Token storage & rotation
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
@@ -13,16 +12,14 @@ import {
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm'; // [NEW]
|
||||
import { Repository } from 'typeorm'; // [NEW]
|
||||
import type { Cache } from 'cache-manager';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
import { User } from '../../modules/user/entities/user.entity.js'; // [NEW] ต้อง Import Entity เพื่อใช้ Repository
|
||||
import { RegisterDto } from './dto/register.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -31,27 +28,31 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
// [NEW] Inject Repository เพื่อใช้ QueryBuilder
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
// [P2-2] Inject RefreshToken Repository
|
||||
@InjectRepository(RefreshToken)
|
||||
private refreshTokenRepository: Repository<RefreshToken>
|
||||
) {}
|
||||
|
||||
// 1. ตรวจสอบ Username/Password
|
||||
async validateUser(username: string, pass: string): Promise<any> {
|
||||
console.log(`🔍 Checking login for: ${username}`);
|
||||
console.log(`🔍 Checking login for: ${username}`); // [DEBUG]
|
||||
// [FIXED] ใช้ createQueryBuilder เพื่อ addSelect field 'password' ที่ถูกซ่อนไว้
|
||||
const user = await this.usersRepository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.password')
|
||||
.addSelect('user.password') // สำคัญ! สั่งให้ดึง column password มาด้วย
|
||||
.where('user.username = :username', { username })
|
||||
.getOne();
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ User not found in database');
|
||||
console.log('❌ User not found in database'); // [DEBUG]
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('✅ User found. Hash from DB:', user.password); // [DEBUG]
|
||||
|
||||
const isMatch = await bcrypt.compare(pass, user.password);
|
||||
console.log(`🔐 Password match result: ${isMatch}`); // [DEBUG]
|
||||
|
||||
// ตรวจสอบว่ามี user และมี password hash หรือไม่
|
||||
if (user && user.password && (await bcrypt.compare(pass, user.password))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -61,7 +62,7 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Login: สร้าง Access & Refresh Token และบันทึกลง DB
|
||||
// 2. Login: สร้าง Access & Refresh Token
|
||||
async login(user: any) {
|
||||
const payload = {
|
||||
username: user.username,
|
||||
@@ -69,20 +70,20 @@ export class AuthService {
|
||||
scope: 'Global',
|
||||
};
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
});
|
||||
|
||||
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
});
|
||||
|
||||
// [P2-2] Store Refresh Token in DB
|
||||
await this.storeRefreshToken(user.user_id, refreshToken);
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
}),
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
access_token: accessToken,
|
||||
@@ -91,28 +92,10 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
// [P2-2] Store Refresh Token Logic
|
||||
private async storeRefreshToken(userId: number, token: string) {
|
||||
// Hash token before storing for security
|
||||
const hash = crypto.createHash('sha256').update(token).digest('hex');
|
||||
const expiresInDays = 7; // Should match JWT_REFRESH_EXPIRATION
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
|
||||
|
||||
const refreshTokenEntity = this.refreshTokenRepository.create({
|
||||
userId,
|
||||
tokenHash: hash,
|
||||
expiresAt,
|
||||
isRevoked: false,
|
||||
});
|
||||
|
||||
await this.refreshTokenRepository.save(refreshTokenEntity);
|
||||
}
|
||||
|
||||
// 3. Register (สำหรับ Admin)
|
||||
async register(userDto: RegisterDto) {
|
||||
const existingUser = await this.userService.findOneByUsername(
|
||||
userDto.username
|
||||
userDto.username,
|
||||
);
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('Username already exists');
|
||||
@@ -127,79 +110,27 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Refresh Token: ตรวจสอบและออก Token ใหม่ (Rotation)
|
||||
// 4. Refresh Token: ออก Token ใหม่
|
||||
async refreshToken(userId: number, refreshToken: string) {
|
||||
// Hash incoming token to match with DB
|
||||
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex');
|
||||
|
||||
// Find token in DB
|
||||
const storedToken = await this.refreshTokenRepository.findOne({
|
||||
where: { tokenHash: hash },
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
if (storedToken.isRevoked) {
|
||||
// Possible token theft! Invalidate all user tokens family
|
||||
await this.revokeAllUserTokens(userId);
|
||||
throw new UnauthorizedException('Refresh token revoked - Security alert');
|
||||
}
|
||||
|
||||
if (storedToken.expiresAt < new Date()) {
|
||||
throw new UnauthorizedException('Refresh token expired');
|
||||
}
|
||||
|
||||
// Valid token -> Rotate it
|
||||
const user = await this.userService.findOne(userId);
|
||||
if (!user) throw new UnauthorizedException('User not found');
|
||||
|
||||
const payload = { username: user.username, sub: user.user_id };
|
||||
|
||||
// Generate NEW tokens
|
||||
const newAccessToken = await this.jwtService.signAsync(payload, {
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
});
|
||||
|
||||
const newRefreshToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
});
|
||||
|
||||
// Revoke OLD token and point to NEW one
|
||||
const newHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(newRefreshToken)
|
||||
.digest('hex');
|
||||
|
||||
storedToken.isRevoked = true;
|
||||
storedToken.replacedByToken = newHash;
|
||||
await this.refreshTokenRepository.save(storedToken);
|
||||
|
||||
// Save NEW token
|
||||
await this.storeRefreshToken(userId, newRefreshToken);
|
||||
|
||||
return {
|
||||
access_token: newAccessToken,
|
||||
refresh_token: newRefreshToken,
|
||||
access_token: accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
// [P2-2] Helper: Revoke all tokens for a user (Security Measure)
|
||||
private async revokeAllUserTokens(userId: number) {
|
||||
await this.refreshTokenRepository.update(
|
||||
{ userId, isRevoked: false },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Logout: Revoke current refresh token & Blacklist Access Token
|
||||
async logout(userId: number, accessToken: string, refreshToken?: string) {
|
||||
// Blacklist Access Token
|
||||
// 5. Logout: นำ Token เข้า Blacklist ใน Redis
|
||||
async logout(userId: number, accessToken: string) {
|
||||
try {
|
||||
const decoded = this.jwtService.decode(accessToken);
|
||||
if (decoded && decoded.exp) {
|
||||
@@ -208,65 +139,13 @@ export class AuthService {
|
||||
await this.cacheManager.set(
|
||||
`blacklist:token:${accessToken}`,
|
||||
true,
|
||||
ttl * 1000
|
||||
ttl * 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore decoding error
|
||||
}
|
||||
|
||||
// [P2-2] Revoke Refresh Token if provided
|
||||
if (refreshToken) {
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(refreshToken)
|
||||
.digest('hex');
|
||||
await this.refreshTokenRepository.update(
|
||||
{ tokenHash: hash },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
|
||||
return { message: 'Logged out successfully' };
|
||||
}
|
||||
|
||||
// [New] Get Active Sessions
|
||||
async getActiveSessions() {
|
||||
// Only return tokens that are NOT revoked and NOT expired
|
||||
const activeTokens = await this.refreshTokenRepository.find({
|
||||
where: {
|
||||
isRevoked: false,
|
||||
},
|
||||
relations: ['user'], // Ensure relations: ['user'] works if RefreshToken entity has relation
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
// Filter expired tokens in memory if query builder is complex, or rely on where clause if possible.
|
||||
// Filter expired tokens
|
||||
return activeTokens
|
||||
.filter((t) => new Date(t.expiresAt) > now)
|
||||
.map((t) => ({
|
||||
id: t.tokenId.toString(),
|
||||
userId: t.userId,
|
||||
user: {
|
||||
username: t.user?.username || 'Unknown',
|
||||
firstName: t.user?.firstName || '',
|
||||
lastName: t.user?.lastName || '',
|
||||
},
|
||||
deviceName: 'Unknown Device', // Not stored in DB
|
||||
ipAddress: 'Unknown IP', // Not stored in DB
|
||||
lastActive: t.createdAt.toISOString(), // Best approximation
|
||||
isCurrent: false, // Cannot determine isCurrent without current session context match
|
||||
}));
|
||||
}
|
||||
|
||||
// [New] Revoke Session by ID
|
||||
async revokeSession(sessionId: number) {
|
||||
return this.refreshTokenRepository.update(
|
||||
{ tokenId: sessionId },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
|
||||
import {
|
||||
Ability,
|
||||
AbilityBuilder,
|
||||
AbilityClass,
|
||||
ExtractSubjectType,
|
||||
InferSubjects,
|
||||
} from '@casl/ability';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
import { UserAssignment } from '../../../modules/user/entities/user-assignment.entity';
|
||||
|
||||
@@ -39,7 +45,7 @@ export class AbilityFactory {
|
||||
* - Level 4: Contract
|
||||
*/
|
||||
createForUser(user: User, context: ScopeContext): AppAbility {
|
||||
const { can, build } = new AbilityBuilder<AppAbility>(
|
||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
|
||||
Ability as AbilityClass<AppAbility>
|
||||
);
|
||||
|
||||
@@ -48,13 +54,12 @@ export class AbilityFactory {
|
||||
return build();
|
||||
}
|
||||
|
||||
// Iterate through user's role assignments
|
||||
// Iterate through user's role assignments
|
||||
user.assignments.forEach((assignment: UserAssignment) => {
|
||||
// Check if assignment matches the current context
|
||||
if (this.matchesScope(assignment, context)) {
|
||||
// Grant permissions from the role
|
||||
assignment.role.permissions?.forEach((permission) => {
|
||||
assignment.role.permissions.forEach((permission) => {
|
||||
const [action, subject] = this.parsePermission(
|
||||
permission.permissionName
|
||||
);
|
||||
@@ -65,10 +70,8 @@ export class AbilityFactory {
|
||||
|
||||
return build({
|
||||
// Detect subject type (for future use with objects)
|
||||
detectSubjectType: (item: any) => {
|
||||
if (typeof item === 'string') return item;
|
||||
return item.constructor;
|
||||
},
|
||||
detectSubjectType: (item) =>
|
||||
item.constructor as ExtractSubjectType<Subjects>,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,17 +120,17 @@ export class AbilityFactory {
|
||||
* "project.view" → ["view", "project"]
|
||||
*/
|
||||
private parsePermission(permissionName: string): [string, string] {
|
||||
// Fallback for special permissions like "system.manage_all"
|
||||
if (permissionName === 'system.manage_all') {
|
||||
return ['manage', 'all'];
|
||||
}
|
||||
|
||||
const parts = permissionName.split('.');
|
||||
if (parts.length === 2) {
|
||||
const [subject, action] = parts;
|
||||
return [action, subject];
|
||||
}
|
||||
|
||||
// Fallback for special permissions like "system.manage_all"
|
||||
if (permissionName === 'system.manage_all') {
|
||||
return ['manage', 'all'];
|
||||
}
|
||||
|
||||
throw new Error(`Invalid permission format: ${permissionName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
description: 'Username (Email)',
|
||||
example: 'admin@np-dms.work',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@ApiProperty({ description: 'Password', example: 'password123' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password!: string;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
|
||||
@Entity('refresh_tokens')
|
||||
export class RefreshToken {
|
||||
@PrimaryGeneratedColumn({ name: 'token_id' })
|
||||
tokenId!: number;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId!: number;
|
||||
|
||||
@Column({ name: 'token_hash', length: 255 })
|
||||
tokenHash!: string;
|
||||
|
||||
@Column({ name: 'expires_at' })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Column({ name: 'is_revoked', default: false })
|
||||
isRevoked!: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Column({ name: 'replaced_by_token', nullable: true, length: 255 })
|
||||
replacedByToken?: string; // For rotation support
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => User, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export class PermissionsGuard implements CanActivate {
|
||||
// Check if user has ALL required permissions
|
||||
const hasPermission = requiredPermissions.every((permission) => {
|
||||
const [action, subject] = this.parsePermission(permission);
|
||||
return ability.can(action as any, subject as any);
|
||||
return ability.can(action, subject);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
|
||||
@@ -47,9 +47,9 @@ export class AuditLog {
|
||||
@Column({ name: 'user_agent', length: 255, nullable: true })
|
||||
userAgent?: string;
|
||||
|
||||
// ✅ [Fix] ทั้งสอง Decorator ต้องระบุ name: 'created_at'
|
||||
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@PrimaryColumn({ name: 'created_at' }) // Composite PK คู่กับ auditId
|
||||
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
|
||||
import {
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
// @PrimaryGeneratedColumn()
|
||||
// id!: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
created_at!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
updated_at!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false })
|
||||
deletedAt?: Date;
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
|
||||
deleted_at!: Date;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
import { User } from '../../../modules/user/entities/user.entity.js';
|
||||
|
||||
@Entity('attachments')
|
||||
export class Attachment {
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileStorageController } from './file-storage.controller';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
|
||||
describe('FileStorageController', () => {
|
||||
let controller: FileStorageController;
|
||||
let mockFileStorageService: Partial<FileStorageService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFileStorageService = {
|
||||
upload: jest.fn(),
|
||||
download: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FileStorageController],
|
||||
providers: [
|
||||
{
|
||||
provide: FileStorageService,
|
||||
useValue: mockFileStorageService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<FileStorageController>(FileStorageController);
|
||||
@@ -29,25 +15,4 @@ describe('FileStorageController', () => {
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should upload a file successfully', async () => {
|
||||
const mockFile = {
|
||||
originalname: 'test.pdf',
|
||||
buffer: Buffer.from('test'),
|
||||
mimetype: 'application/pdf',
|
||||
size: 100,
|
||||
} as Express.Multer.File;
|
||||
|
||||
const mockResult = { attachment_id: 1, originalFilename: 'test.pdf' };
|
||||
(mockFileStorageService.upload as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const mockReq = { user: { userId: 1, username: 'testuser' } };
|
||||
const result = await controller.uploadFile(mockFile, mockReq as any);
|
||||
|
||||
expect(mockFileStorageService.upload).toHaveBeenCalledWith(mockFile, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||
|
||||
// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว
|
||||
interface RequestWithUser {
|
||||
@@ -47,10 +47,10 @@ export class FileStorageController {
|
||||
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
|
||||
}),
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
@Request() req: RequestWithUser
|
||||
@Request() req: RequestWithUser,
|
||||
) {
|
||||
// ส่ง userId จาก Token ไปด้วย
|
||||
return this.fileStorageService.upload(file, req.user.userId);
|
||||
@@ -63,7 +63,7 @@ export class FileStorageController {
|
||||
@Get(':id/download')
|
||||
async downloadFile(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Res({ passthrough: true }) res: Response
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const { stream, attachment } = await this.fileStorageService.download(id);
|
||||
|
||||
@@ -87,7 +87,7 @@ export class FileStorageController {
|
||||
@Delete(':id')
|
||||
async deleteFile(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Request() req: RequestWithUser
|
||||
@Request() req: RequestWithUser,
|
||||
) {
|
||||
// ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ
|
||||
await this.fileStorageService.delete(id, req.user.userId);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { FileStorageController } from './file-storage.controller.js';
|
||||
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -1,142 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as fs from 'fs-extra';
|
||||
import {
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
// Mock fs-extra
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('FileStorageService', () => {
|
||||
let service: FileStorageService;
|
||||
let attachmentRepo: Repository<Attachment>;
|
||||
|
||||
const mockAttachment = {
|
||||
id: 1,
|
||||
originalFilename: 'test.pdf',
|
||||
storedFilename: 'uuid.pdf',
|
||||
filePath: '/permanent/2024/12/uuid.pdf',
|
||||
fileSize: 1024,
|
||||
uploadedByUserId: 1,
|
||||
} as Attachment;
|
||||
|
||||
const mockFile = {
|
||||
originalname: 'test.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 1024,
|
||||
buffer: Buffer.from('test-content'),
|
||||
} as Express.Multer.File;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FileStorageService,
|
||||
{
|
||||
provide: getRepositoryToken(Attachment),
|
||||
useValue: {
|
||||
create: jest.fn().mockReturnValue(mockAttachment),
|
||||
save: jest.fn().mockResolvedValue(mockAttachment),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn((key) => {
|
||||
if (key === 'NODE_ENV') return 'test';
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
providers: [FileStorageService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FileStorageService>(FileStorageService);
|
||||
attachmentRepo = module.get(getRepositoryToken(Attachment));
|
||||
|
||||
jest.clearAllMocks();
|
||||
(fs.ensureDirSync as jest.Mock).mockReturnValue(true);
|
||||
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true);
|
||||
(fs.move as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.remove as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('upload', () => {
|
||||
it('should save file to temp and create DB record', async () => {
|
||||
const result = await service.upload(mockFile, 1);
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalled();
|
||||
expect(attachmentRepo.create).toHaveBeenCalled();
|
||||
expect(attachmentRepo.save).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if write fails', async () => {
|
||||
(fs.writeFile as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('Write error')
|
||||
);
|
||||
await expect(service.upload(mockFile, 1)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should move files to permanent storage', async () => {
|
||||
const tempIds = ['uuid-1'];
|
||||
const mockAttachments = [
|
||||
{
|
||||
...mockAttachment,
|
||||
isTemporary: true,
|
||||
tempId: 'uuid-1',
|
||||
filePath: '/temp/uuid.pdf',
|
||||
},
|
||||
];
|
||||
|
||||
(attachmentRepo.find as jest.Mock).mockResolvedValue(mockAttachments);
|
||||
|
||||
await service.commit(tempIds);
|
||||
|
||||
expect(fs.ensureDir).toHaveBeenCalled();
|
||||
expect(fs.move).toHaveBeenCalled();
|
||||
expect(attachmentRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show warning if file counts mismatch', async () => {
|
||||
(attachmentRepo.find as jest.Mock).mockResolvedValue([]);
|
||||
await expect(service.commit(['uuid-1'])).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete file if user owns it', async () => {
|
||||
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
|
||||
|
||||
await service.delete(1, 1);
|
||||
|
||||
expect(fs.remove).toHaveBeenCalled();
|
||||
expect(attachmentRepo.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if user does not own file', async () => {
|
||||
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
|
||||
await expect(service.delete(1, 999)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -5,25 +5,25 @@ import {
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSIONS_KEY } from '../decorators/require-permission.decorator';
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js';
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class RbacGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private userService: UserService
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()]
|
||||
const requiredPermission = this.reflector.getAllAndOverride<string>(
|
||||
PERMISSION_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
if (!requiredPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -34,20 +34,19 @@ export class RbacGuard implements CanActivate {
|
||||
}
|
||||
|
||||
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
|
||||
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
|
||||
const userPermissions = await this.userService.getUserPermissions(
|
||||
user.user_id // ✅ FIX: ใช้ user_id ตาม Entity field name
|
||||
user.userId,
|
||||
);
|
||||
|
||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? (User ต้องมีครบทุกสิทธิ์)
|
||||
const hasPermission = requiredPermissions.every((req) =>
|
||||
userPermissions.some(
|
||||
(p) => p === req || p === 'system.manage_all' // Superadmin ทะลุทุกสิทธิ์
|
||||
)
|
||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม?
|
||||
const hasPermission = userPermissions.some(
|
||||
(p) => p === requiredPermission || p === 'system.manage_all', // Superadmin ทะลุทุกสิทธิ์
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException(
|
||||
`You do not have permission: ${requiredPermissions.join(', ')}`
|
||||
`You do not have permission: ${requiredPermission}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface Response<T> {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
data: T;
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -20,29 +19,14 @@ export class TransformInterceptor<T>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler
|
||||
next: CallHandler,
|
||||
): Observable<Response<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data: any) => {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
|
||||
// Handle Pagination Response (Standardize)
|
||||
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
|
||||
if (data && data.data && data.meta) {
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: data.message || 'Success',
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: data?.message || 'Success',
|
||||
data: data?.result || data,
|
||||
};
|
||||
})
|
||||
map((data) => ({
|
||||
statusCode: context.switchToHttp().getResponse().statusCode,
|
||||
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
|
||||
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Organization } from '../../modules/organization/entities/organization.entity';
|
||||
import { Organization } from '../../modules/organizations/entities/organization.entity';
|
||||
|
||||
export async function seedOrganizations(dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(Organization);
|
||||
|
||||
@@ -1,54 +1,45 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { Role, RoleScope } from '../../modules/user/entities/role.entity';
|
||||
import { UserAssignment } from '../../modules/user/entities/user-assignment.entity';
|
||||
import { User } from '../../modules/users/entities/user.entity';
|
||||
import { Role } from '../../modules/auth/entities/role.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export async function seedUsers(dataSource: DataSource) {
|
||||
const userRepo = dataSource.getRepository(User);
|
||||
const roleRepo = dataSource.getRepository(Role);
|
||||
const assignmentRepo = dataSource.getRepository(UserAssignment);
|
||||
|
||||
// Create Roles
|
||||
const rolesData = [
|
||||
{
|
||||
roleName: 'Superadmin',
|
||||
scope: RoleScope.GLOBAL,
|
||||
description:
|
||||
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',
|
||||
},
|
||||
{
|
||||
roleName: 'Org Admin',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Document Control',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Editor',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',
|
||||
},
|
||||
{
|
||||
roleName: 'Viewer',
|
||||
scope: RoleScope.PROJECT,
|
||||
description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',
|
||||
},
|
||||
{
|
||||
roleName: 'Project Manager',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',
|
||||
},
|
||||
{
|
||||
roleName: 'Contract Admin',
|
||||
scope: RoleScope.CONTRACT,
|
||||
description:
|
||||
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',
|
||||
},
|
||||
@@ -58,7 +49,6 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
for (const r of rolesData) {
|
||||
let role = await roleRepo.findOneBy({ roleName: r.roleName });
|
||||
if (!role) {
|
||||
// @ts-ignore
|
||||
role = await roleRepo.save(roleRepo.create(r));
|
||||
}
|
||||
roleMap.set(r.roleName, role);
|
||||
@@ -97,30 +87,20 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
];
|
||||
|
||||
const salt = await bcrypt.genSalt();
|
||||
const password = await bcrypt.hash('password123', salt); // Default password
|
||||
const passwordHash = await bcrypt.hash('password123', salt); // Default password
|
||||
|
||||
for (const u of usersData) {
|
||||
let user = await userRepo.findOneBy({ username: u.username });
|
||||
if (!user) {
|
||||
user = userRepo.create({
|
||||
const exists = await userRepo.findOneBy({ username: u.username });
|
||||
if (!exists) {
|
||||
const user = userRepo.create({
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
password, // Fixed: password instead of passwordHash
|
||||
passwordHash,
|
||||
roles: [roleMap.get(u.roleName)],
|
||||
});
|
||||
user = await userRepo.save(user);
|
||||
|
||||
// Create Assignment
|
||||
const role = roleMap.get(u.roleName);
|
||||
if (role) {
|
||||
const assignment = assignmentRepo.create({
|
||||
user,
|
||||
role,
|
||||
assignedAt: new Date(),
|
||||
});
|
||||
await assignmentRepo.save(assignment);
|
||||
}
|
||||
await userRepo.save(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,24 +22,11 @@ async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
// 🛡️ 2. Security (Helmet & CORS)
|
||||
// ปรับ CSP ให้รองรับ Swagger UI
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false,
|
||||
})
|
||||
);
|
||||
app.use(helmet());
|
||||
|
||||
// ตั้งค่า CORS (ใน Production ควรระบุ origin ให้ชัดเจนจาก Config)
|
||||
app.enableCors({
|
||||
origin: configService.get<string>('CORS_ORIGIN') || true,
|
||||
origin: true, // หรือ configService.get('CORS_ORIGIN')
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true,
|
||||
});
|
||||
@@ -60,7 +47,7 @@ async function bootstrap() {
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true, // ช่วยแปลง Type ใน Query Params
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// ลงทะเบียน Global Interceptor และ Filter ที่เราสร้างไว้
|
||||
@@ -86,9 +73,9 @@ async function bootstrap() {
|
||||
|
||||
// 🚀 7. Start Server
|
||||
const port = configService.get<number>('PORT') || 3001;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`Application is running on: ${await app.getUrl()}/api`);
|
||||
logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`);
|
||||
}
|
||||
void bootstrap();
|
||||
bootstrap();
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
|
||||
@Controller('audit-logs')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class AuditLogController {
|
||||
constructor(private readonly auditLogService: AuditLogService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('audit-log.view')
|
||||
findAll(@Query() query: any) {
|
||||
return this.auditLogService.findAll(query);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuditLogController } from './audit-log.controller';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AuditLog]), UserModule],
|
||||
controllers: [AuditLogController],
|
||||
providers: [AuditLogService],
|
||||
exports: [AuditLogService],
|
||||
})
|
||||
export class AuditLogModule {}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService {
|
||||
constructor(
|
||||
@InjectRepository(AuditLog)
|
||||
private readonly auditLogRepository: Repository<AuditLog>
|
||||
) {}
|
||||
|
||||
async findAll(query: any) {
|
||||
const { page = 1, limit = 20, entityName, action, userId } = query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const queryBuilder =
|
||||
this.auditLogRepository.createQueryBuilder('audit_logs'); // Aliased as 'audit_logs' matching table name usually, or just 'log'
|
||||
|
||||
if (entityName) {
|
||||
queryBuilder.andWhere('audit_logs.entityName LIKE :entityName', {
|
||||
entityName: `%${entityName}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (action) {
|
||||
queryBuilder.andWhere('audit_logs.action = :action', { action });
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
queryBuilder.andWhere('audit_logs.userId = :userId', { userId });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('audit_logs.createdAt', 'DESC').skip(skip).take(limit);
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,55 +5,36 @@ import {
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
@Entity('permissions')
|
||||
export class Permission extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'permission_id' })
|
||||
export class Permission {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'permission_name', length: 100, unique: true })
|
||||
permissionName!: string;
|
||||
@Column({ name: 'permission_code', length: 50, unique: true })
|
||||
permissionCode!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ name: 'module', length: 50, nullable: true })
|
||||
module?: string;
|
||||
@Column({ name: 'resource', length: 50 })
|
||||
resource!: string;
|
||||
|
||||
@Column({
|
||||
name: 'scope_level',
|
||||
type: 'enum',
|
||||
enum: ['GLOBAL', 'ORG', 'PROJECT'],
|
||||
nullable: true,
|
||||
})
|
||||
scopeLevel?: 'GLOBAL' | 'ORG' | 'PROJECT';
|
||||
|
||||
@Column({ name: 'is_active', default: true, type: 'tinyint' })
|
||||
isActive!: boolean;
|
||||
@Column({ name: 'action', length: 50 })
|
||||
action!: string;
|
||||
}
|
||||
|
||||
@Entity('roles')
|
||||
export class Role extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'role_id' })
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'role_name', length: 100, unique: true })
|
||||
@Column({ name: 'role_name', length: 50, unique: true })
|
||||
roleName!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['Global', 'Organization', 'Project', 'Contract'],
|
||||
default: 'Global',
|
||||
})
|
||||
scope!: 'Global' | 'Organization' | 'Project' | 'Contract';
|
||||
|
||||
@Column({ name: 'is_system', default: false })
|
||||
isSystem!: boolean;
|
||||
|
||||
@ManyToMany(() => Permission)
|
||||
@JoinTable({
|
||||
name: 'role_permissions',
|
||||
|
||||
@@ -13,7 +13,7 @@ import { User } from '../user/entities/user.entity';
|
||||
import { CreateCirculationDto } from './dto/create-circulation.dto';
|
||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
||||
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
|
||||
@Injectable()
|
||||
export class CirculationService {
|
||||
@@ -37,9 +37,9 @@ export class CirculationService {
|
||||
|
||||
try {
|
||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||
const result = await this.numberingService.generateNextNumber({
|
||||
const circulationNo = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
||||
originatorOrganizationId: user.primaryOrganizationId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
@@ -51,7 +51,7 @@ export class CirculationService {
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
correspondenceId: createDto.correspondenceId,
|
||||
circulationNo: result.number,
|
||||
circulationNo: circulationNo,
|
||||
subject: createDto.subject,
|
||||
statusCode: 'OPEN',
|
||||
createdByUserId: user.user_id,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Circulation } from './circulation.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
|
||||
@Entity('circulation_routings')
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||
import { CirculationRouting } from './circulation-routing.entity';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ContractService } from './contract.service';
|
||||
import { ContractController } from './contract.controller';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity';
|
||||
import { ProjectModule } from '../project/project.module'; // Likely needed for Project entity or service
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Contract, ContractOrganization]),
|
||||
ProjectModule,
|
||||
],
|
||||
controllers: [ContractController],
|
||||
providers: [ContractService],
|
||||
exports: [ContractService],
|
||||
})
|
||||
export class ContractModule {}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class SearchContractDto {
|
||||
@ApiPropertyOptional({ description: 'Search term (code or name)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Project ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
projectId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Page number', default: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Items per page', default: 100 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
limit?: number = 100;
|
||||
}
|
||||
@@ -23,14 +23,13 @@ export class CorrespondenceWorkflowService {
|
||||
private readonly revisionRepo: Repository<CorrespondenceRevision>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private readonly statusRepo: Repository<CorrespondenceStatus>,
|
||||
private readonly dataSource: DataSource
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async submitWorkflow(
|
||||
correspondenceId: number,
|
||||
userId: number,
|
||||
userRoles: string[], // [FIX] Added roles for DSL requirements check
|
||||
note?: string
|
||||
note?: string,
|
||||
) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -45,7 +44,7 @@ export class CorrespondenceWorkflowService {
|
||||
|
||||
if (!revision) {
|
||||
throw new NotFoundException(
|
||||
`Correspondence Revision for ID ${correspondenceId} not found`
|
||||
`Correspondence Revision for ID ${correspondenceId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ export class CorrespondenceWorkflowService {
|
||||
this.WORKFLOW_CODE,
|
||||
'correspondence_revision',
|
||||
revision.id.toString(),
|
||||
context
|
||||
context,
|
||||
);
|
||||
|
||||
const transitionResult = await this.workflowEngine.processTransition(
|
||||
@@ -75,7 +74,7 @@ export class CorrespondenceWorkflowService {
|
||||
'SUBMIT',
|
||||
userId,
|
||||
note || 'Initial Submission',
|
||||
{ roles: userRoles } // [FIX] Pass roles for DSL requirements check
|
||||
{},
|
||||
);
|
||||
|
||||
await this.syncStatus(revision, transitionResult.nextState, queryRunner);
|
||||
@@ -98,14 +97,14 @@ export class CorrespondenceWorkflowService {
|
||||
async processAction(
|
||||
instanceId: string,
|
||||
userId: number,
|
||||
dto: WorkflowTransitionDto
|
||||
dto: WorkflowTransitionDto,
|
||||
) {
|
||||
const result = await this.workflowEngine.processTransition(
|
||||
instanceId,
|
||||
dto.action,
|
||||
userId,
|
||||
dto.comment,
|
||||
dto.payload
|
||||
dto.payload,
|
||||
);
|
||||
|
||||
// ✅ FIX: Method exists now
|
||||
@@ -126,7 +125,7 @@ export class CorrespondenceWorkflowService {
|
||||
private async syncStatus(
|
||||
revision: CorrespondenceRevision,
|
||||
workflowState: string,
|
||||
queryRunner?: any
|
||||
queryRunner?: any,
|
||||
) {
|
||||
const statusMap: Record<string, string> = {
|
||||
DRAFT: 'DRAFT',
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CorrespondenceController } from './correspondence.controller';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
|
||||
describe('CorrespondenceController', () => {
|
||||
let controller: CorrespondenceController;
|
||||
let mockCorrespondenceService: Partial<CorrespondenceService>;
|
||||
let mockWorkflowService: Partial<CorrespondenceWorkflowService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCorrespondenceService = {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
getReferences: jest.fn(),
|
||||
addReference: jest.fn(),
|
||||
removeReference: jest.fn(),
|
||||
};
|
||||
|
||||
mockWorkflowService = {
|
||||
submitWorkflow: jest.fn(),
|
||||
processAction: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CorrespondenceController],
|
||||
providers: [
|
||||
{
|
||||
provide: CorrespondenceService,
|
||||
useValue: mockCorrespondenceService,
|
||||
},
|
||||
{
|
||||
provide: CorrespondenceWorkflowService,
|
||||
useValue: mockWorkflowService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(RbacGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CorrespondenceController>(CorrespondenceController);
|
||||
});
|
||||
@@ -50,68 +15,4 @@ describe('CorrespondenceController', () => {
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return correspondences', async () => {
|
||||
const mockResult = [{ id: 1 }];
|
||||
(mockCorrespondenceService.findAll as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const result = await controller.findAll({});
|
||||
|
||||
expect(mockCorrespondenceService.findAll).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a correspondence', async () => {
|
||||
const mockCorr = { id: 1, correspondenceNumber: 'TEST-001' };
|
||||
(mockCorrespondenceService.create as jest.Mock).mockResolvedValue(
|
||||
mockCorr
|
||||
);
|
||||
|
||||
const mockReq = { user: { user_id: 1 } };
|
||||
const createDto = {
|
||||
projectId: 1,
|
||||
typeId: 1,
|
||||
title: 'Test Subject',
|
||||
};
|
||||
|
||||
const result = await controller.create(
|
||||
createDto as Parameters<typeof controller.create>[0],
|
||||
mockReq as Parameters<typeof controller.create>[1]
|
||||
);
|
||||
|
||||
expect(mockCorrespondenceService.create).toHaveBeenCalledWith(
|
||||
createDto,
|
||||
mockReq.user
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
it('should submit a correspondence to workflow', async () => {
|
||||
const mockResult = { instanceId: 'inst-1', currentState: 'IN_REVIEW' };
|
||||
(mockWorkflowService.submitWorkflow as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const mockReq = { user: { user_id: 1 } };
|
||||
const result = await controller.submit(
|
||||
1,
|
||||
{ note: 'Test note' },
|
||||
mockReq as Parameters<typeof controller.submit>[2]
|
||||
);
|
||||
|
||||
expect(mockWorkflowService.submitWorkflow).toHaveBeenCalledWith(
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
'Test note'
|
||||
);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,209 +5,91 @@ import {
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
Delete,
|
||||
Put,
|
||||
Param, // <--- ✅ 1. เพิ่ม Param
|
||||
ParseIntPipe, // <--- ✅ 2. เพิ่ม ParseIntPipe
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
import { CorrespondenceService } from './correspondence.service.js';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto.js'; // <--- ✅ 3. เพิ่ม Import DTO นี้
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
// ... imports ...
|
||||
import { AddReferenceDto } from './dto/add-reference.dto.js';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
|
||||
import { Query, Delete } from '@nestjs/common'; // เพิ่ม Query, Delete
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
|
||||
@ApiTags('Correspondences')
|
||||
@Controller('correspondences')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@ApiBearerAuth()
|
||||
export class CorrespondenceController {
|
||||
constructor(
|
||||
private readonly correspondenceService: CorrespondenceService,
|
||||
private readonly workflowService: CorrespondenceWorkflowService
|
||||
) {}
|
||||
constructor(private readonly correspondenceService: CorrespondenceService) {}
|
||||
|
||||
@Post(':id/workflow/action')
|
||||
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
|
||||
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
|
||||
@RequirePermission('workflow.action_review')
|
||||
@RequirePermission('workflow.action_review') // สิทธิ์ในการกดอนุมัติ/ตรวจสอบ
|
||||
processAction(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() actionDto: WorkflowActionDto,
|
||||
@Request()
|
||||
req: Request & {
|
||||
user: {
|
||||
user_id: number;
|
||||
assignments?: Array<{ role: { roleName: string } }>;
|
||||
};
|
||||
}
|
||||
@Request() req: any,
|
||||
) {
|
||||
// Extract roles from user assignments for DSL requirements check
|
||||
const userRoles =
|
||||
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
|
||||
|
||||
// Use Unified Workflow Engine via CorrespondenceWorkflowService
|
||||
if (!actionDto.instanceId) {
|
||||
throw new Error('instanceId is required for workflow action');
|
||||
}
|
||||
|
||||
return this.workflowService.processAction(
|
||||
actionDto.instanceId,
|
||||
req.user.user_id,
|
||||
{
|
||||
action: actionDto.action,
|
||||
comment: actionDto.comment,
|
||||
payload: { ...actionDto.payload, roles: userRoles },
|
||||
}
|
||||
);
|
||||
return this.correspondenceService.processAction(id, actionDto, req.user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new correspondence' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Correspondence created successfully.',
|
||||
type: CreateCorrespondenceDto,
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.create', 'correspondence')
|
||||
create(
|
||||
@Body() createDto: CreateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
return this.correspondenceService.create(
|
||||
createDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
}
|
||||
|
||||
@Post('preview-number')
|
||||
@ApiOperation({ summary: 'Preview next document number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return preview number and status.',
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
previewNumber(
|
||||
@Body() createDto: CreateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
return this.correspondenceService.previewDocumentNumber(
|
||||
createDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
|
||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
||||
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
|
||||
return this.correspondenceService.create(createDto, req.user);
|
||||
}
|
||||
|
||||
// ✅ ปรับปรุง findAll ให้รับ Query Params
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search correspondences' })
|
||||
@ApiResponse({ status: 200, description: 'Return list of correspondences.' })
|
||||
@RequirePermission('document.view')
|
||||
findAll(@Query() searchDto: SearchCorrespondenceDto) {
|
||||
return this.correspondenceService.findAll(searchDto);
|
||||
}
|
||||
|
||||
// ✅ เพิ่ม Endpoint นี้ครับ
|
||||
@Post(':id/submit')
|
||||
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Correspondence submitted successfully.',
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.submit', 'correspondence')
|
||||
@RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้
|
||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
||||
submit(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() submitDto: SubmitCorrespondenceDto,
|
||||
@Request()
|
||||
req: Request & {
|
||||
user: {
|
||||
user_id: number;
|
||||
assignments?: Array<{ role: { roleName: string } }>;
|
||||
};
|
||||
}
|
||||
@Request() req: any,
|
||||
) {
|
||||
// Extract roles from user assignments
|
||||
const userRoles =
|
||||
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
|
||||
|
||||
// Use Unified Workflow Engine - pass user roles for DSL requirements check
|
||||
return this.workflowService.submitWorkflow(
|
||||
return this.correspondenceService.submit(
|
||||
id,
|
||||
req.user.user_id,
|
||||
userRoles,
|
||||
submitDto.note
|
||||
submitDto.templateId,
|
||||
req.user,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get correspondence by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Return correspondence details.' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.correspondenceService.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Update correspondence (Draft only)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Correspondence updated successfully.',
|
||||
})
|
||||
@RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit'
|
||||
@Audit('correspondence.update', 'correspondence')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateDto: UpdateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
return this.correspondenceService.update(
|
||||
id,
|
||||
updateDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
}
|
||||
// --- REFERENCES ---
|
||||
|
||||
@Get(':id/references')
|
||||
@ApiOperation({ summary: 'Get referenced documents' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return list of referenced documents.',
|
||||
})
|
||||
@RequirePermission('document.view')
|
||||
getReferences(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.correspondenceService.getReferences(id);
|
||||
}
|
||||
|
||||
@Post(':id/references')
|
||||
@ApiOperation({ summary: 'Add reference to another document' })
|
||||
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
@RequirePermission('document.edit') // ต้องมีสิทธิ์แก้ไขถึงจะเพิ่ม Ref ได้
|
||||
addReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: AddReferenceDto
|
||||
@Body() dto: AddReferenceDto,
|
||||
) {
|
||||
return this.correspondenceService.addReference(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/references/:targetId')
|
||||
@ApiOperation({ summary: 'Remove reference' })
|
||||
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
removeReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('targetId', ParseIntPipe) targetId: number
|
||||
@Param('targetId', ParseIntPipe) targetId: number,
|
||||
) {
|
||||
return this.correspondenceService.removeReference(id, targetId);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CorrespondenceController } from './correspondence.controller';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
import { CorrespondenceController } from './correspondence.controller.js';
|
||||
import { CorrespondenceService } from './correspondence.service.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity.js';
|
||||
import { Correspondence } from './entities/correspondence.entity.js';
|
||||
// Import Entities ใหม่
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity.js';
|
||||
import { RoutingTemplateStep } from './entities/routing-template-step.entity.js';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity.js';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module.js'; // ต้องใช้ตอน Create
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
||||
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
|
||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
||||
// Controllers & Services
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service'; // Register Service นี้
|
||||
|
||||
// Dependent Modules
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
/**
|
||||
* CorrespondenceModule
|
||||
*
|
||||
* NOTE: RoutingTemplate and RoutingTemplateStep have been deprecated.
|
||||
* All workflow operations now use the Unified Workflow Engine.
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
@@ -33,18 +27,19 @@ import { SearchModule } from '../search/search.module';
|
||||
CorrespondenceRevision,
|
||||
CorrespondenceType,
|
||||
CorrespondenceStatus,
|
||||
CorrespondenceReference,
|
||||
CorrespondenceRecipient,
|
||||
Organization,
|
||||
RoutingTemplate, // <--- ลงทะเบียน
|
||||
RoutingTemplateStep, // <--- ลงทะเบียน
|
||||
CorrespondenceRouting, // <--- ลงทะเบียน
|
||||
CorrespondenceReference, // <--- ลงทะเบียน
|
||||
]),
|
||||
DocumentNumberingModule,
|
||||
JsonSchemaModule,
|
||||
UserModule,
|
||||
WorkflowEngineModule,
|
||||
SearchModule,
|
||||
DocumentNumberingModule, // Import เพื่อขอเลขที่เอกสาร
|
||||
JsonSchemaModule, // Import เพื่อ Validate JSON
|
||||
UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้
|
||||
WorkflowEngineModule, // <--- Import WorkflowEngine
|
||||
SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่
|
||||
],
|
||||
controllers: [CorrespondenceController],
|
||||
providers: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||
exports: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||
exports: [CorrespondenceService],
|
||||
})
|
||||
export class CorrespondenceModule {}
|
||||
|
||||
@@ -1,270 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
describe('CorrespondenceService', () => {
|
||||
let service: CorrespondenceService;
|
||||
let numberingService: DocumentNumberingService;
|
||||
let correspondenceRepo: any;
|
||||
let revisionRepo: any;
|
||||
let dataSource: any;
|
||||
|
||||
const createMockRepository = () => ({
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue(null),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
})),
|
||||
});
|
||||
|
||||
const mockDataSource = {
|
||||
createQueryRunner: jest.fn(() => ({
|
||||
connect: jest.fn(),
|
||||
startTransaction: jest.fn(),
|
||||
commitTransaction: jest.fn(),
|
||||
rollbackTransaction: jest.fn(),
|
||||
release: jest.fn(),
|
||||
manager: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
})),
|
||||
getRepository: jest.fn(() => createMockRepository()),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CorrespondenceService,
|
||||
{
|
||||
provide: getRepositoryToken(Correspondence),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CorrespondenceRevision),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CorrespondenceType),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CorrespondenceStatus),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CorrespondenceReference),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Organization),
|
||||
useValue: createMockRepository(),
|
||||
},
|
||||
{
|
||||
provide: DocumentNumberingService,
|
||||
useValue: {
|
||||
generateNextNumber: jest.fn(),
|
||||
updateNumberForDraft: jest.fn(),
|
||||
previewNextNumber: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: JsonSchemaService,
|
||||
useValue: { validate: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: WorkflowEngineService,
|
||||
useValue: { createInstance: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
getUserPermissions: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
{
|
||||
provide: SearchService,
|
||||
useValue: { indexDocument: jest.fn() },
|
||||
},
|
||||
],
|
||||
providers: [CorrespondenceService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CorrespondenceService>(CorrespondenceService);
|
||||
numberingService = module.get<DocumentNumberingService>(
|
||||
DocumentNumberingService
|
||||
);
|
||||
correspondenceRepo = module.get(getRepositoryToken(Correspondence));
|
||||
revisionRepo = module.get(getRepositoryToken(CorrespondenceRevision));
|
||||
dataSource = module.get(DataSource);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should NOT regenerate number if critical fields unchanged', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
}; // Status 5 = Draft handled by logic?
|
||||
// Mock status repo to return DRAFT
|
||||
// But strict logic: revision.statusId check
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
const mockStatus = { id: 5, statusCode: 'DRAFT' };
|
||||
// Need to set statusRepo mock behavior... simplified here for brevity or assume defaults
|
||||
// Injecting internal access to statusRepo is hard without `module.get` if I didn't save it.
|
||||
// Let's assume it passes check for now.
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
// Update DTO with same values
|
||||
const updateDto = {
|
||||
projectId: 1,
|
||||
disciplineId: 3,
|
||||
// recipients missing -> imply no change
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
// Check that updateNumberForDraft was NOT called
|
||||
expect(numberingService.updateNumberForDraft).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should regenerate number if Project ID changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1, // Old Project
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
const updateDto = {
|
||||
projectId: 2, // New Project -> Change!
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
it('should regenerate number if Document Type changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2, // Old Type
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
const updateDto = {
|
||||
typeId: 999, // New Type
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should regenerate number if Recipient Organization changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], // Old Recipient 99
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
jest
|
||||
.spyOn(service['orgRepo'], 'findOne')
|
||||
.mockResolvedValue({ id: 88, organizationCode: 'NEW-ORG' } as any);
|
||||
|
||||
const updateDto = {
|
||||
recipients: [{ type: 'TO', organizationId: 88 }], // New Recipient 88
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,38 +9,34 @@ import {
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Repository, DataSource, Like, In } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
// Entitie
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
/**
|
||||
* CorrespondenceService - Document management (CRUD)
|
||||
*
|
||||
* NOTE: Workflow operations (submit, processAction) have been moved to
|
||||
* CorrespondenceWorkflowService which uses the Unified Workflow Engine.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CorrespondenceService {
|
||||
private readonly logger = new Logger(CorrespondenceService.name);
|
||||
@@ -54,10 +50,12 @@ export class CorrespondenceService {
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
@InjectRepository(RoutingTemplate)
|
||||
private templateRepo: Repository<RoutingTemplate>,
|
||||
@InjectRepository(CorrespondenceRouting)
|
||||
private routingRepo: Repository<CorrespondenceRouting>,
|
||||
@InjectRepository(CorrespondenceReference)
|
||||
private referenceRepo: Repository<CorrespondenceReference>,
|
||||
@InjectRepository(Organization)
|
||||
private orgRepo: Repository<Organization>,
|
||||
|
||||
private numberingService: DocumentNumberingService,
|
||||
private jsonSchemaService: JsonSchemaService,
|
||||
@@ -113,9 +111,9 @@ export class CorrespondenceService {
|
||||
if (createDto.details) {
|
||||
try {
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.warn(
|
||||
`Schema validation warning for ${type.typeCode}: ${(error as Error).message}`
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,38 +125,24 @@ export class CorrespondenceService {
|
||||
try {
|
||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Organization Entity
|
||||
|
||||
// [v1.5.1] Extract recipient organization from recipients array (Primary TO)
|
||||
const toRecipient = createDto.recipients?.find((r) => r.type === 'TO');
|
||||
const recipientOrganizationId = toRecipient?.organizationId;
|
||||
|
||||
let recipientCode = '';
|
||||
if (recipientOrganizationId) {
|
||||
const recOrg = await this.orgRepo.findOne({
|
||||
where: { id: recipientOrganizationId },
|
||||
});
|
||||
if (recOrg) recipientCode = recOrg.organizationCode;
|
||||
}
|
||||
|
||||
// [FIXED] เรียกใช้แบบ Object Context ตาม Requirement 6B
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorOrganizationId: userOrgId,
|
||||
originatorId: userOrgId,
|
||||
typeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
subTypeId: createDto.subTypeId,
|
||||
recipientOrganizationId, // [v1.5.1] Pass recipient for document number format
|
||||
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: orgCode,
|
||||
RECIPIENT_CODE: recipientCode,
|
||||
REC_CODE: recipientCode,
|
||||
},
|
||||
});
|
||||
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber.number,
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
disciplineId: createDto.disciplineId, // บันทึก Discipline ลง DB
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
isInternal: createDto.isInternal || false,
|
||||
@@ -172,32 +156,16 @@ export class CorrespondenceService {
|
||||
revisionLabel: 'A',
|
||||
isCurrent: true,
|
||||
statusId: statusDraft.id,
|
||||
subject: createDto.subject,
|
||||
body: createDto.body,
|
||||
remarks: createDto.remarks,
|
||||
dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
details: createDto.details,
|
||||
createdBy: user.user_id,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// Save Recipients
|
||||
if (createDto.recipients && createDto.recipients.length > 0) {
|
||||
const recipients = createDto.recipients.map((r) =>
|
||||
queryRunner.manager.create(CorrespondenceRecipient, {
|
||||
correspondenceId: savedCorr.id,
|
||||
recipientOrganizationId: r.organizationId,
|
||||
recipientType: r.type,
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(recipients);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// Start Workflow Instance (non-blocking)
|
||||
// [NEW V1.5.1] Start Workflow Instance (After Commit)
|
||||
try {
|
||||
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
||||
await this.workflowEngine.createInstance(
|
||||
@@ -213,15 +181,16 @@ export class CorrespondenceService {
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber.number} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||
);
|
||||
// Non-blocking: Document is created, but workflow might not be active.
|
||||
}
|
||||
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'correspondence',
|
||||
docNumber: docNumber.number,
|
||||
title: createDto.subject,
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
@@ -243,35 +212,17 @@ export class CorrespondenceService {
|
||||
}
|
||||
}
|
||||
|
||||
// ... (method อื่นๆ คงเดิม)
|
||||
async findAll(searchDto: SearchCorrespondenceDto = {}) {
|
||||
const {
|
||||
search,
|
||||
typeId,
|
||||
projectId,
|
||||
statusId,
|
||||
page = 1,
|
||||
limit = 10,
|
||||
} = searchDto;
|
||||
const skip = (page - 1) * limit;
|
||||
const { search, typeId, projectId, statusId } = searchDto;
|
||||
|
||||
// Change: Query from Revision Repo
|
||||
const query = this.revisionRepo
|
||||
.createQueryBuilder('rev')
|
||||
.leftJoinAndSelect('rev.correspondence', 'corr')
|
||||
const query = this.correspondenceRepo
|
||||
.createQueryBuilder('corr')
|
||||
.leftJoinAndSelect('corr.revisions', 'rev')
|
||||
.leftJoinAndSelect('corr.type', 'type')
|
||||
.leftJoinAndSelect('corr.project', 'project')
|
||||
.leftJoinAndSelect('corr.originator', 'org')
|
||||
.leftJoinAndSelect('rev.status', 'status');
|
||||
|
||||
// Filter by Revision Status
|
||||
const revStatus = searchDto.revisionStatus || 'CURRENT';
|
||||
|
||||
if (revStatus === 'CURRENT') {
|
||||
query.where('rev.isCurrent = :isCurrent', { isCurrent: true });
|
||||
} else if (revStatus === 'OLD') {
|
||||
query.where('rev.isCurrent = :isCurrent', { isCurrent: false });
|
||||
}
|
||||
// If 'ALL', no filter needed on isCurrent
|
||||
.where('rev.isCurrent = :isCurrent', { isCurrent: true });
|
||||
|
||||
if (projectId) {
|
||||
query.andWhere('corr.projectId = :projectId', { projectId });
|
||||
@@ -287,25 +238,14 @@ export class CorrespondenceService {
|
||||
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)',
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
// Default Sort: Latest Created
|
||||
query.orderBy('rev.createdAt', 'DESC').skip(skip).take(limit);
|
||||
query.orderBy('corr.createdAt', 'DESC');
|
||||
|
||||
const [items, total] = await query.getManyAndCount();
|
||||
|
||||
return {
|
||||
data: items,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
@@ -317,8 +257,6 @@ export class CorrespondenceService {
|
||||
'type',
|
||||
'project',
|
||||
'originator',
|
||||
'recipients',
|
||||
'recipients.recipientOrganization', // [v1.5.1] Fixed relation name
|
||||
],
|
||||
});
|
||||
|
||||
@@ -328,6 +266,182 @@ export class CorrespondenceService {
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
async submit(correspondenceId: number, templateId: number, user: User) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
}
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision) {
|
||||
throw new NotFoundException('Current revision not found');
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: templateId },
|
||||
relations: ['steps'],
|
||||
order: { steps: { sequence: 'ASC' } },
|
||||
});
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined'
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const firstStep = template.steps[0];
|
||||
|
||||
const routing = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: 1,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: firstStep.toOrganizationId,
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
});
|
||||
await queryRunner.manager.save(routing);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return routing;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User
|
||||
) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence)
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
const currentRouting = await this.routingRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: currentRevision.id,
|
||||
status: 'SENT',
|
||||
},
|
||||
order: { sequence: 'DESC' },
|
||||
relations: ['toOrganization'],
|
||||
});
|
||||
|
||||
if (!currentRouting) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found for this document'
|
||||
);
|
||||
}
|
||||
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId'
|
||||
);
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: currentRouting.templateId },
|
||||
relations: ['steps'],
|
||||
});
|
||||
|
||||
if (!template || !template.steps) {
|
||||
throw new InternalServerErrorException('Template definition not found');
|
||||
}
|
||||
|
||||
const totalSteps = template.steps.length;
|
||||
const currentSeq = currentRouting.sequence;
|
||||
|
||||
const result = this.workflowEngine.processAction(
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
this.logger.warn(
|
||||
`Next step ${result.nextStepSequence} not found in template`
|
||||
);
|
||||
} else {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
CorrespondenceRouting,
|
||||
{
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: result.nextStepSequence,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: nextStepConfig.toOrganizationId,
|
||||
stepPurpose: nextStepConfig.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
}
|
||||
);
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: 'Action processed successfully', result };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async addReference(id: number, dto: AddReferenceDto) {
|
||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||
const target = await this.correspondenceRepo.findOne({
|
||||
@@ -385,236 +499,4 @@ export class CorrespondenceService {
|
||||
|
||||
return { outgoing, incoming };
|
||||
}
|
||||
|
||||
async update(id: number, updateDto: UpdateCorrespondenceDto, user: User) {
|
||||
// 1. Find Current Revision
|
||||
const revision = await this.revisionRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: id,
|
||||
isCurrent: true,
|
||||
},
|
||||
relations: ['correspondence'],
|
||||
});
|
||||
|
||||
if (!revision) {
|
||||
throw new NotFoundException(
|
||||
`Current revision for correspondence ${id} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check Permission
|
||||
if (revision.statusId) {
|
||||
const status = await this.statusRepo.findOne({
|
||||
where: { id: revision.statusId },
|
||||
});
|
||||
if (status && status.statusCode !== 'DRAFT') {
|
||||
throw new BadRequestException('Only DRAFT documents can be updated');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update Correspondence Entity if needed
|
||||
const correspondenceUpdate: DeepPartial<Correspondence> = {};
|
||||
if (updateDto.disciplineId)
|
||||
correspondenceUpdate.disciplineId = updateDto.disciplineId;
|
||||
if (updateDto.projectId)
|
||||
correspondenceUpdate.projectId = updateDto.projectId;
|
||||
if (updateDto.originatorId)
|
||||
correspondenceUpdate.originatorId = updateDto.originatorId;
|
||||
|
||||
if (Object.keys(correspondenceUpdate).length > 0) {
|
||||
await this.correspondenceRepo.update(id, correspondenceUpdate);
|
||||
}
|
||||
|
||||
// 4. Update Revision Entity
|
||||
const revisionUpdate: DeepPartial<CorrespondenceRevision> = {};
|
||||
if (updateDto.subject) revisionUpdate.subject = updateDto.subject;
|
||||
if (updateDto.body) revisionUpdate.body = updateDto.body;
|
||||
if (updateDto.remarks) revisionUpdate.remarks = updateDto.remarks;
|
||||
// Format Date correctly if string
|
||||
if (updateDto.dueDate) revisionUpdate.dueDate = new Date(updateDto.dueDate);
|
||||
if (updateDto.description)
|
||||
revisionUpdate.description = updateDto.description;
|
||||
if (updateDto.details) revisionUpdate.details = updateDto.details;
|
||||
|
||||
if (Object.keys(revisionUpdate).length > 0) {
|
||||
await this.revisionRepo.update(revision.id, revisionUpdate);
|
||||
}
|
||||
|
||||
// 5. Update Recipients if provided
|
||||
if (updateDto.recipients) {
|
||||
const recipientRepo = this.dataSource.getRepository(
|
||||
CorrespondenceRecipient
|
||||
);
|
||||
await recipientRepo.delete({ correspondenceId: id });
|
||||
|
||||
const newRecipients = updateDto.recipients.map((r) =>
|
||||
recipientRepo.create({
|
||||
correspondenceId: id,
|
||||
recipientOrganizationId: r.organizationId,
|
||||
recipientType: r.type,
|
||||
})
|
||||
);
|
||||
await recipientRepo.save(newRecipients);
|
||||
}
|
||||
|
||||
// 6. Regenerate Document Number if structural fields changed (Recipient, Discipline, Type, Project)
|
||||
// AND it is a DRAFT.
|
||||
|
||||
// Fetch fresh data for context and comparison
|
||||
const currentCorr = await this.correspondenceRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['type', 'recipients', 'recipients.recipientOrganization'],
|
||||
});
|
||||
|
||||
if (currentCorr) {
|
||||
const currentToRecipient = currentCorr.recipients?.find(
|
||||
(r) => r.recipientType === 'TO'
|
||||
);
|
||||
const currentRecipientId = currentToRecipient?.recipientOrganizationId;
|
||||
|
||||
// Check for ACTUAL value changes
|
||||
const isProjectChanged =
|
||||
updateDto.projectId !== undefined &&
|
||||
updateDto.projectId !== currentCorr.projectId;
|
||||
const isOriginatorChanged =
|
||||
updateDto.originatorId !== undefined &&
|
||||
updateDto.originatorId !== currentCorr.originatorId;
|
||||
const isDisciplineChanged =
|
||||
updateDto.disciplineId !== undefined &&
|
||||
updateDto.disciplineId !== currentCorr.disciplineId;
|
||||
const isTypeChanged =
|
||||
updateDto.typeId !== undefined &&
|
||||
updateDto.typeId !== currentCorr.correspondenceTypeId;
|
||||
|
||||
let isRecipientChanged = false;
|
||||
let newRecipientId: number | undefined;
|
||||
|
||||
if (updateDto.recipients) {
|
||||
// Safe check for 'type' or 'recipientType' (mismatch safeguard)
|
||||
const newToRecipient = updateDto.recipients.find(
|
||||
(r: any) => r.type === 'TO' || r.recipientType === 'TO'
|
||||
);
|
||||
newRecipientId = newToRecipient?.organizationId;
|
||||
|
||||
if (newRecipientId !== currentRecipientId) {
|
||||
isRecipientChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isProjectChanged ||
|
||||
isDisciplineChanged ||
|
||||
isTypeChanged ||
|
||||
isRecipientChanged ||
|
||||
isOriginatorChanged
|
||||
) {
|
||||
const targetRecipientId = isRecipientChanged
|
||||
? newRecipientId
|
||||
: currentRecipientId;
|
||||
|
||||
// Resolve Recipient Code for the NEW context
|
||||
let recipientCode = '';
|
||||
if (targetRecipientId) {
|
||||
const recOrg = await this.orgRepo.findOne({
|
||||
where: { id: targetRecipientId },
|
||||
});
|
||||
if (recOrg) recipientCode = recOrg.organizationCode;
|
||||
}
|
||||
|
||||
const orgCode = 'ORG'; // Placeholder - should be fetched from Originator if needed in future
|
||||
|
||||
// Prepare Contexts
|
||||
const oldCtx = {
|
||||
projectId: currentCorr.projectId,
|
||||
originatorOrganizationId: currentCorr.originatorId ?? 0,
|
||||
typeId: currentCorr.correspondenceTypeId,
|
||||
disciplineId: currentCorr.disciplineId,
|
||||
recipientOrganizationId: currentRecipientId,
|
||||
year: new Date().getFullYear(),
|
||||
};
|
||||
|
||||
const newCtx = {
|
||||
projectId: updateDto.projectId ?? currentCorr.projectId,
|
||||
originatorOrganizationId:
|
||||
updateDto.originatorId ?? currentCorr.originatorId ?? 0,
|
||||
typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId,
|
||||
disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId,
|
||||
recipientOrganizationId: targetRecipientId,
|
||||
year: new Date().getFullYear(),
|
||||
userId: user.user_id, // Pass User ID for Audit
|
||||
customTokens: {
|
||||
TYPE_CODE: currentCorr.type?.typeCode || '',
|
||||
ORG_CODE: orgCode,
|
||||
RECIPIENT_CODE: recipientCode,
|
||||
REC_CODE: recipientCode,
|
||||
},
|
||||
};
|
||||
|
||||
// If Type Changed, need NEW Type Code
|
||||
if (isTypeChanged) {
|
||||
const newType = await this.typeRepo.findOne({
|
||||
where: { id: newCtx.typeId },
|
||||
});
|
||||
if (newType) newCtx.customTokens.TYPE_CODE = newType.typeCode;
|
||||
}
|
||||
|
||||
const newDocNumber = await this.numberingService.updateNumberForDraft(
|
||||
currentCorr.correspondenceNumber,
|
||||
oldCtx,
|
||||
newCtx
|
||||
);
|
||||
|
||||
await this.correspondenceRepo.update(id, {
|
||||
correspondenceNumber: newDocNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async previewDocumentNumber(createDto: CreateCorrespondenceDto, user: User) {
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { id: createDto.typeId },
|
||||
});
|
||||
if (!type) throw new NotFoundException('Document Type not found');
|
||||
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
const fullUser = await this.userService.findOne(user.user_id);
|
||||
if (fullUser) userOrgId = fullUser.primaryOrganizationId;
|
||||
}
|
||||
|
||||
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
|
||||
// Allow impersonation for preview
|
||||
userOrgId = createDto.originatorId;
|
||||
}
|
||||
|
||||
// Extract recipient from recipients array
|
||||
const toRecipient = createDto.recipients?.find((r) => r.type === 'TO');
|
||||
const recipientOrganizationId = toRecipient?.organizationId;
|
||||
|
||||
let recipientCode = '';
|
||||
if (recipientOrganizationId) {
|
||||
const recOrg = await this.orgRepo.findOne({
|
||||
where: { id: recipientOrganizationId },
|
||||
});
|
||||
if (recOrg) recipientCode = recOrg.organizationCode;
|
||||
}
|
||||
|
||||
return this.numberingService.previewNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorOrganizationId: userOrgId!,
|
||||
typeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
subTypeId: createDto.subTypeId,
|
||||
recipientOrganizationId,
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: type.typeCode,
|
||||
RECIPIENT_CODE: recipientCode,
|
||||
REC_CODE: recipientCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AddReferenceDto {
|
||||
@ApiProperty({
|
||||
description: 'Target Correspondence ID to reference',
|
||||
example: 20,
|
||||
})
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
targetId!: number;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// File: src/modules/correspondence/dto/create-correspondence.dto.ts
|
||||
import {
|
||||
IsInt,
|
||||
IsString,
|
||||
@@ -5,99 +6,43 @@ import {
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsObject,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateCorrespondenceDto {
|
||||
@ApiProperty({ description: 'Project ID', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ description: 'Document Type ID', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
typeId!: number; // ID ของประเภทเอกสาร (เช่น RFA, LETTER)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Discipline ID', example: 2 })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
disciplineId?: number; // [Req 6B] สาขางาน (เช่น GEN, STR)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sub Type ID', example: 3 })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
subTypeId?: number; // [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA)
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Correspondence Subject',
|
||||
example: 'Monthly Progress Report',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string;
|
||||
title!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Body/Content',
|
||||
example: '<p>...</p>',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
body?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Remarks',
|
||||
example: 'Note...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
remarks?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Due Date',
|
||||
example: '2025-12-06T00:00:00Z',
|
||||
})
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
dueDate?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Correspondence Description',
|
||||
example: 'Detailed report...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Additional details (JSON)',
|
||||
example: { key: 'value' },
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is internal document?', default: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isInternal?: boolean;
|
||||
|
||||
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Originator Organization ID (for impersonation)',
|
||||
example: 1,
|
||||
})
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
originatorId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Recipients',
|
||||
example: [{ organizationId: 1, type: 'TO' }],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,24 @@
|
||||
import { IsOptional, IsString, IsInt } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer'; // <--- ✅ Import จาก class-transformer
|
||||
|
||||
export class SearchCorrespondenceDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search term (Title or Document Number)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
search?: string; // ค้นหาจาก Title หรือ Number
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Document Type ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
typeId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Project ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
projectId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Status ID' })
|
||||
// status อาจจะซับซ้อนหน่อยเพราะอยู่ที่ Revision แต่ใส่ไว้ก่อน
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
statusId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Revision Filter: CURRENT (default), ALL, OLD',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
revisionStatus?: 'CURRENT' | 'ALL' | 'OLD';
|
||||
|
||||
@ApiPropertyOptional({ description: 'Page number (default 1)', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Items per page (default 10)',
|
||||
default: 10,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for submitting correspondence to workflow
|
||||
* Uses Unified Workflow Engine - no templateId required
|
||||
*/
|
||||
export class SubmitCorrespondenceDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Optional note for the submission',
|
||||
example: 'Submitting for review',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
note?: string;
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
templateId!: number;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCorrespondenceDto } from './create-correspondence.dto';
|
||||
|
||||
export class UpdateCorrespondenceDto extends PartialType(
|
||||
CreateCorrespondenceDto
|
||||
) {}
|
||||
@@ -1,61 +1,15 @@
|
||||
import { IsEnum, IsString, IsOptional, IsUUID, IsInt } from 'class-validator';
|
||||
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
|
||||
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
|
||||
|
||||
/**
|
||||
* DTO for processing workflow actions
|
||||
*
|
||||
* Supports both:
|
||||
* - New Unified Workflow Engine (uses instanceId)
|
||||
* - Legacy RFA workflow (uses returnToSequence)
|
||||
*/
|
||||
export class WorkflowActionDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Workflow Instance ID (UUID) - for Unified Workflow Engine',
|
||||
example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
instanceId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Workflow Action',
|
||||
enum: ['APPROVE', 'REJECT', 'RETURN', 'CANCEL', 'ACKNOWLEDGE'],
|
||||
})
|
||||
@IsEnum(WorkflowAction)
|
||||
action!: WorkflowAction;
|
||||
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Review comments',
|
||||
example: 'Approved with note...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use 'comment' instead
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Review comments (deprecated, use comment)',
|
||||
example: 'Approved with note...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comments?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sequence to return to (only for RETURN action in legacy RFA)',
|
||||
example: 1,
|
||||
})
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
returnToSequence?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Additional payload data',
|
||||
example: { priority: 'HIGH' },
|
||||
})
|
||||
@IsOptional()
|
||||
payload?: Record<string, unknown>;
|
||||
returnToSequence?: number; // ใช้กรณี action = RETURN
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
|
||||
@Entity('correspondence_recipients')
|
||||
export class CorrespondenceRecipient {
|
||||
@PrimaryColumn({ name: 'correspondence_id' })
|
||||
correspondenceId!: number;
|
||||
|
||||
@PrimaryColumn({ name: 'recipient_organization_id' })
|
||||
recipientOrganizationId!: number;
|
||||
|
||||
@PrimaryColumn({ name: 'recipient_type', type: 'enum', enum: ['TO', 'CC'] })
|
||||
recipientType!: 'TO' | 'CC';
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Correspondence, (corr) => corr.recipients, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
correspondence!: Correspondence;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'recipient_organization_id' })
|
||||
recipientOrganization!: Organization;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { Correspondence } from './correspondence.entity.js';
|
||||
|
||||
@Entity('correspondence_references')
|
||||
export class CorrespondenceReference {
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { Correspondence } from './correspondence.entity.js';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
|
||||
@Entity('correspondence_revisions')
|
||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||
@@ -35,24 +35,15 @@ export class CorrespondenceRevision {
|
||||
@Column({ name: 'correspondence_status_id' })
|
||||
statusId!: number;
|
||||
|
||||
@Column({ length: 500 })
|
||||
subject!: string;
|
||||
@Column({ length: 255 })
|
||||
title!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
body?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
remarks?: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type
|
||||
|
||||
@Column({ name: 'schema_version', default: 1 })
|
||||
schemaVersion!: number;
|
||||
|
||||
// ✅ [New] Virtual Column: ดึง Project ID จาก JSON details
|
||||
@Column({
|
||||
name: 'v_ref_project_id',
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { RoutingTemplate } from './routing-template.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity.js';
|
||||
|
||||
@Entity('correspondence_routings')
|
||||
export class CorrespondenceRouting {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Contract } from '../../contract/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
import { Contract } from '../../project/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
import { CorrespondenceType } from './correspondence-type.entity'; // ปรับ path ตามจริง
|
||||
|
||||
@Entity('correspondence_sub_types')
|
||||
|
||||
@@ -8,13 +8,11 @@ import {
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from './correspondence-type.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Discipline } from '../../master/entities/discipline.entity';
|
||||
import { Project } from '../../project/entities/project.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { CorrespondenceType } from './correspondence-type.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js'; // เดี๋ยวสร้าง
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
@@ -70,9 +68,9 @@ export class Correspondence {
|
||||
creator?: User;
|
||||
|
||||
// [New V1.5.1]
|
||||
@ManyToOne(() => Discipline)
|
||||
@ManyToOne('Discipline')
|
||||
@JoinColumn({ name: 'discipline_id' })
|
||||
discipline?: Discipline;
|
||||
discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import.
|
||||
|
||||
// One Correspondence has Many Revisions
|
||||
@OneToMany(
|
||||
@@ -80,11 +78,4 @@ export class Correspondence {
|
||||
(revision) => revision.correspondence
|
||||
)
|
||||
revisions?: CorrespondenceRevision[];
|
||||
|
||||
@OneToMany(
|
||||
() => CorrespondenceRecipient,
|
||||
(recipient) => recipient.correspondence,
|
||||
{ cascade: true }
|
||||
)
|
||||
recipients?: CorrespondenceRecipient[];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
// File: src/modules/correspondence/entities/routing-template-step.entity.ts
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { RoutingTemplate } from './routing-template.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { Role } from '../../user/entities/role.entity.js';
|
||||
|
||||
/**
|
||||
* @deprecated This entity is deprecated and will be removed in future versions.
|
||||
* Use WorkflowDefinition from the Unified Workflow Engine instead.
|
||||
*
|
||||
* This entity is kept for backward compatibility and historical data.
|
||||
* Relations have been removed to prevent TypeORM errors.
|
||||
*/
|
||||
@Entity('correspondence_routing_template_steps')
|
||||
export class RoutingTemplateStep {
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -21,12 +24,27 @@ export class RoutingTemplateStep {
|
||||
@Column({ name: 'to_organization_id' })
|
||||
toOrganizationId!: number;
|
||||
|
||||
@Column({ name: 'step_purpose', length: 50, default: 'FOR_REVIEW' })
|
||||
@Column({ name: 'role_id', nullable: true })
|
||||
roleId?: number;
|
||||
|
||||
@Column({ name: 'step_purpose', default: 'FOR_REVIEW' })
|
||||
stepPurpose!: string;
|
||||
|
||||
@Column({ name: 'expected_days', default: 7 })
|
||||
expectedDays!: number;
|
||||
@Column({ name: 'expected_days', nullable: true })
|
||||
expectedDays?: number;
|
||||
|
||||
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||
// template?: RoutingTemplate;
|
||||
// Relations
|
||||
@ManyToOne(() => RoutingTemplate, (template) => template.steps, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template?: RoutingTemplate;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'to_organization_id' })
|
||||
toOrganization?: Organization;
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
@JoinColumn({ name: 'role_id' })
|
||||
role?: Role;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js'; // ถ้าไม่ได้ใช้ BaseEntity ก็ลบออกแล้วใส่ createdAt เอง
|
||||
import { RoutingTemplateStep } from './routing-template-step.entity.js'; // เดี๋ยวสร้าง
|
||||
|
||||
/**
|
||||
* @deprecated This entity is deprecated and will be removed in future versions.
|
||||
* Use WorkflowDefinition from the Unified Workflow Engine instead.
|
||||
*
|
||||
* This entity is kept for backward compatibility and historical data.
|
||||
* The relation to RoutingTemplateStep has been removed to prevent TypeORM errors.
|
||||
*/
|
||||
@Entity('correspondence_routing_templates')
|
||||
export class RoutingTemplate {
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -19,14 +14,14 @@ export class RoutingTemplate {
|
||||
description?: string;
|
||||
|
||||
@Column({ name: 'project_id', nullable: true })
|
||||
projectId?: number;
|
||||
projectId?: number; // NULL = แม่แบบทั่วไป
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ type: 'json', nullable: true, name: 'workflow_config' })
|
||||
workflowConfig?: Record<string, unknown>;
|
||||
workflowConfig?: any;
|
||||
|
||||
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||
// steps?: RoutingTemplateStep[];
|
||||
@OneToMany(() => RoutingTemplateStep, (step) => step.template)
|
||||
steps?: RoutingTemplateStep[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'document_number', length: 50, unique: true })
|
||||
documentNumber!: string;
|
||||
|
||||
@Column({ length: 255 })
|
||||
subject!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
body!: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
type!: string;
|
||||
|
||||
@Column({ length: 50, default: 'Draft' })
|
||||
status!: string;
|
||||
|
||||
@Column({ name: 'created_by_id' })
|
||||
createdById!: number;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'created_by_id' })
|
||||
createdBy!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// File: src/modules/dashboard/dashboard.controller.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard API Endpoints
|
||||
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
// Guards & Decorators
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// Service
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
// DTOs
|
||||
import { GetActivityDto, GetPendingDto } from './dto';
|
||||
|
||||
@ApiTags('Dashboard')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('dashboard')
|
||||
export class DashboardController {
|
||||
constructor(private readonly dashboardService: DashboardService) {}
|
||||
|
||||
/**
|
||||
* ดึงสถิติ Dashboard
|
||||
*/
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: 'Get dashboard statistics' })
|
||||
async getStats(@CurrentUser() user: User) {
|
||||
return this.dashboardService.getStats(user.user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Recent Activity
|
||||
*/
|
||||
@Get('activity')
|
||||
@ApiOperation({ summary: 'Get recent activity' })
|
||||
async getActivity(@CurrentUser() user: User, @Query() query: GetActivityDto) {
|
||||
return this.dashboardService.getActivity(user.user_id, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Pending Tasks
|
||||
*/
|
||||
@Get('pending')
|
||||
@ApiOperation({ summary: 'Get pending tasks for current user' })
|
||||
async getPending(@CurrentUser() user: User, @Query() query: GetPendingDto) {
|
||||
return this.dashboardService.getPending(user.user_id, query);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// File: src/modules/dashboard/dashboard.module.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Module
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import { WorkflowInstance } from '../workflow-engine/entities/workflow-instance.entity';
|
||||
|
||||
// Controller & Service
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Correspondence, AuditLog, WorkflowInstance]),
|
||||
],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
exports: [DashboardService],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
@@ -1,214 +0,0 @@
|
||||
// File: src/modules/dashboard/dashboard.service.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Business Logic
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import {
|
||||
WorkflowInstance,
|
||||
WorkflowStatus,
|
||||
} from '../workflow-engine/entities/workflow-instance.entity';
|
||||
|
||||
// DTOs
|
||||
import {
|
||||
DashboardStatsDto,
|
||||
GetActivityDto,
|
||||
ActivityItemDto,
|
||||
GetPendingDto,
|
||||
PendingTaskItemDto,
|
||||
} from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly logger = new Logger(DashboardService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
@InjectRepository(AuditLog)
|
||||
private auditLogRepo: Repository<AuditLog>,
|
||||
@InjectRepository(WorkflowInstance)
|
||||
private workflowInstanceRepo: Repository<WorkflowInstance>,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
* ดึงสถิติ Dashboard
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
*/
|
||||
async getStats(userId: number): Promise<DashboardStatsDto> {
|
||||
this.logger.debug(`Getting dashboard stats for user ${userId}`);
|
||||
|
||||
// นับจำนวนเอกสารทั้งหมด
|
||||
const totalDocuments = await this.correspondenceRepo.count();
|
||||
|
||||
// นับจำนวนเอกสารเดือนนี้
|
||||
const startOfMonth = new Date();
|
||||
startOfMonth.setDate(1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
const documentsThisMonth = await this.correspondenceRepo
|
||||
.createQueryBuilder('c')
|
||||
.where('c.createdAt >= :startOfMonth', { startOfMonth })
|
||||
.getCount();
|
||||
|
||||
// นับงานที่รอ Approve (Workflow Active)
|
||||
const pendingApprovals = await this.workflowInstanceRepo.count({
|
||||
where: { status: WorkflowStatus.ACTIVE },
|
||||
});
|
||||
|
||||
// นับ RFA ทั้งหมด (correspondence_type_id = RFA type)
|
||||
// ใช้ Raw Query เพราะต้อง JOIN กับ correspondence_types
|
||||
const rfaCountResult = await this.dataSource.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM correspondences c
|
||||
JOIN correspondence_types ct ON c.correspondence_type_id = ct.id
|
||||
WHERE ct.type_code = 'RFA'
|
||||
`);
|
||||
const totalRfas = parseInt(rfaCountResult[0]?.count || '0', 10);
|
||||
|
||||
// นับ Circulation ทั้งหมด
|
||||
const circulationsCountResult = await this.dataSource.query(`
|
||||
SELECT COUNT(*) as count FROM circulations
|
||||
`);
|
||||
const totalCirculations = parseInt(
|
||||
circulationsCountResult[0]?.count || '0',
|
||||
10
|
||||
);
|
||||
|
||||
// นับเอกสารที่อนุมัติแล้ว (APPROVED)
|
||||
// NOTE: อาจจะต้องปรับ logic ตาม Business ว่า "อนุมัติ" หมายถึงอะไร
|
||||
// เบื้องต้นนับจาก CorrespondenceStatus ที่เป็น 'APPROVED' หรือ 'CODE 1'
|
||||
// หรือนับจาก Workflow ที่ Completed และ Action เป็น APPROVE
|
||||
// เพื่อความง่ายในเบื้องต้น นับจาก CorrespondenceRevision ที่มี status 'APPROVED' (ถ้ามี)
|
||||
// หรือนับจาก RFA ที่มี Approve Code
|
||||
|
||||
// สำหรับ LCBP3 นับ RFA ที่ approveCodeId ไม่ใช่ null (หรือ check status code = APR/FAP)
|
||||
// และ Correspondence ทั่วไปที่มีสถานะ Completed
|
||||
// เพื่อความรวดเร็ว ใช้วิธีนับ Revision ที่ isCurrent = 1 และ statusCode = 'APR' (Approved)
|
||||
|
||||
// Check status code 'APR' exists
|
||||
const aprStatusCount = await this.dataSource.query(`
|
||||
SELECT COUNT(r.id) as count
|
||||
FROM correspondence_revisions r
|
||||
JOIN correspondence_status s ON r.correspondence_status_id = s.id
|
||||
WHERE r.is_current = 1 AND s.status_code IN ('APR', 'CMP')
|
||||
`);
|
||||
const approved = parseInt(aprStatusCount[0]?.count || '0', 10);
|
||||
|
||||
return {
|
||||
totalDocuments,
|
||||
documentsThisMonth,
|
||||
pendingApprovals,
|
||||
approved,
|
||||
totalRfas,
|
||||
totalCirculations,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Activity ล่าสุด
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
* @param dto - Query params
|
||||
*/
|
||||
async getActivity(
|
||||
userId: number,
|
||||
dto: GetActivityDto
|
||||
): Promise<ActivityItemDto[]> {
|
||||
const { limit = 10 } = dto;
|
||||
this.logger.debug(`Getting recent activity for user ${userId}`);
|
||||
|
||||
// ดึง Recent Audit Logs
|
||||
const logs = await this.auditLogRepo
|
||||
.createQueryBuilder('log')
|
||||
.leftJoin('log.user', 'user')
|
||||
.select([
|
||||
'log.action',
|
||||
'log.entityType',
|
||||
'log.entityId',
|
||||
'log.detailsJson',
|
||||
'log.createdAt',
|
||||
'user.username',
|
||||
])
|
||||
.orderBy('log.createdAt', 'DESC')
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
|
||||
return logs.map((log) => ({
|
||||
action: log.action,
|
||||
entityType: log.entityType,
|
||||
entityId: log.entityId,
|
||||
details: log.detailsJson,
|
||||
createdAt: log.createdAt,
|
||||
username: log.user?.username,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Pending Tasks ของ User
|
||||
* ใช้ v_user_tasks view จาก Database
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
* @param dto - Query params
|
||||
*/
|
||||
async getPending(
|
||||
userId: number,
|
||||
dto: GetPendingDto
|
||||
): Promise<{
|
||||
data: PendingTaskItemDto[];
|
||||
meta: { total: number; page: number; limit: number };
|
||||
}> {
|
||||
const { page = 1, limit = 10 } = dto;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
this.logger.debug(`Getting pending tasks for user ${userId}`);
|
||||
|
||||
// ใช้ Raw Query เพราะต้อง Query จาก View และ Filter ด้วย JSON
|
||||
// v_user_tasks มี assignee_ids_json สำหรับ Filter
|
||||
// MariaDB 11.8: ใช้ JSON_SEARCH แทน CAST AS JSON
|
||||
const userIdNum = Number(userId);
|
||||
|
||||
const [tasks, countResult] = await Promise.all([
|
||||
this.dataSource.query(
|
||||
`
|
||||
SELECT
|
||||
instance_id as instanceId,
|
||||
workflow_code as workflowCode,
|
||||
current_state as currentState,
|
||||
entity_type as entityType,
|
||||
entity_id as entityId,
|
||||
document_number as documentNumber,
|
||||
subject,
|
||||
assigned_at as assignedAt
|
||||
FROM v_user_tasks
|
||||
WHERE
|
||||
JSON_SEARCH(assignee_ids_json, 'one', ?) IS NOT NULL
|
||||
OR owner_id = ?
|
||||
ORDER BY assigned_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[userIdNum, userIdNum, limit, offset]
|
||||
),
|
||||
this.dataSource.query(
|
||||
`
|
||||
SELECT COUNT(*) as total
|
||||
FROM v_user_tasks
|
||||
WHERE
|
||||
JSON_SEARCH(assignee_ids_json, 'one', ?) IS NOT NULL
|
||||
OR owner_id = ?
|
||||
`,
|
||||
[userIdNum, userIdNum]
|
||||
),
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult[0]?.total || '0', 10);
|
||||
|
||||
return {
|
||||
data: tasks,
|
||||
meta: { total, page, limit },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// File: src/modules/dashboard/dto/dashboard-stats.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Stats Response
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Dashboard Statistics
|
||||
*/
|
||||
export class DashboardStatsDto {
|
||||
@ApiProperty({ description: 'จำนวนเอกสารทั้งหมด', example: 150 })
|
||||
totalDocuments!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวนเอกสารเดือนนี้', example: 25 })
|
||||
documentsThisMonth!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวนงานที่รออนุมัติ', example: 12 })
|
||||
pendingApprovals!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวนเอกสารที่อนุมัติแล้ว', example: 100 })
|
||||
approved!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวน RFA ทั้งหมด', example: 45 })
|
||||
totalRfas!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวน Circulation ทั้งหมด', example: 30 })
|
||||
totalCirculations!: number;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// File: src/modules/dashboard/dto/get-activity.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Query params ของ Activity endpoint
|
||||
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Query params ของ GET /dashboard/activity
|
||||
*/
|
||||
export class GetActivityDto {
|
||||
@ApiPropertyOptional({ description: 'จำนวนรายการที่ต้องการ', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
limit?: number = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Activity Item
|
||||
*/
|
||||
export class ActivityItemDto {
|
||||
@ApiPropertyOptional({ description: 'Action ที่กระทำ' })
|
||||
action!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ประเภท Entity' })
|
||||
entityType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ID ของ Entity' })
|
||||
entityId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'รายละเอียด' })
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'วันที่กระทำ' })
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ชื่อผู้ใช้' })
|
||||
username?: string;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// File: src/modules/dashboard/dto/get-pending.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Query params ของ Pending endpoint
|
||||
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Query params ของ GET /dashboard/pending
|
||||
*/
|
||||
export class GetPendingDto {
|
||||
@ApiPropertyOptional({ description: 'หน้าที่ต้องการ', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: 'จำนวนรายการต่อหน้า', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
limit?: number = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Pending Task Item
|
||||
*/
|
||||
export class PendingTaskItemDto {
|
||||
@ApiPropertyOptional({ description: 'Instance ID ของ Workflow' })
|
||||
instanceId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow Code' })
|
||||
workflowCode!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'State ปัจจุบัน' })
|
||||
currentState!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ประเภทเอกสาร' })
|
||||
entityType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ID ของเอกสาร' })
|
||||
entityId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'เลขที่เอกสาร' })
|
||||
documentNumber!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'หัวข้อเรื่อง' })
|
||||
subject!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'วันที่ได้รับมอบหมาย' })
|
||||
assignedAt!: Date;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// File: src/modules/dashboard/dto/index.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ export DTOs ทั้งหมด
|
||||
|
||||
export * from './dashboard-stats.dto';
|
||||
export * from './get-activity.dto';
|
||||
export * from './get-pending.dto';
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { DocumentNumberingService } from '../services/document-numbering.service';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../../common/decorators/require-permission.decorator';
|
||||
|
||||
@ApiTags('Admin / Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin/document-numbering')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class DocumentNumberingAdminController {
|
||||
constructor(private readonly service: DocumentNumberingService) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Template Management
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('templates')
|
||||
@ApiOperation({ summary: 'Get all document numbering templates' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async getTemplates(@Query('projectId') projectId?: number) {
|
||||
if (projectId) {
|
||||
return this.service.getTemplatesByProject(projectId);
|
||||
}
|
||||
return this.service.getTemplates();
|
||||
}
|
||||
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async saveTemplate(@Body() dto: any) {
|
||||
return this.service.saveTemplate(dto);
|
||||
}
|
||||
|
||||
@Delete('templates/:id')
|
||||
@ApiOperation({ summary: 'Delete a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
|
||||
await this.service.deleteTemplate(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Metrics & Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('metrics')
|
||||
@ApiOperation({ summary: 'Get numbering usage metrics and logs' })
|
||||
@RequirePermission('system.view_logs')
|
||||
async getMetrics() {
|
||||
const audit = await this.service.getAuditLogs(50);
|
||||
const errors = await this.service.getErrorLogs(50);
|
||||
return { audit, errors };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Admin Operations
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('manual-override')
|
||||
@ApiOperation({
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
@RequirePermission('system.manage_settings')
|
||||
async manualOverride(@Body() dto: any) {
|
||||
return this.service.manualOverride(dto);
|
||||
}
|
||||
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async voidAndReplace(@Body() dto: any) {
|
||||
return this.service.voidAndReplace(dto);
|
||||
}
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async cancelNumber(@Body() dto: any) {
|
||||
return this.service.cancelNumber(dto);
|
||||
}
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async bulkImport(@Body() items: any[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../../common/decorators/require-permission.decorator';
|
||||
import { DocumentNumberingService } from '../services/document-numbering.service';
|
||||
import { PreviewNumberDto } from '../dto/preview-number.dto';
|
||||
|
||||
@ApiTags('Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@Controller('document-numbering')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class DocumentNumberingController {
|
||||
constructor(private readonly numberingService: DocumentNumberingService) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('logs/audit')
|
||||
@ApiOperation({ summary: 'Get document generation audit logs' })
|
||||
@ApiResponse({ status: 200, description: 'List of audit logs' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@RequirePermission('system.view_logs')
|
||||
getAuditLogs(@Query('limit') limit?: number) {
|
||||
return this.numberingService.getAuditLogs(limit ? Number(limit) : 100);
|
||||
}
|
||||
|
||||
@Get('logs/errors')
|
||||
@ApiOperation({ summary: 'Get document generation error logs' })
|
||||
@ApiResponse({ status: 200, description: 'List of error logs' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@RequirePermission('system.view_logs')
|
||||
getErrorLogs(@Query('limit') limit?: number) {
|
||||
return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Sequences / Counters
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('sequences')
|
||||
@ApiOperation({ summary: 'Get all number sequences/counters' })
|
||||
@ApiResponse({ status: 200, description: 'List of counter sequences' })
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
@RequirePermission('correspondence.read')
|
||||
getSequences(@Query('projectId') projectId?: number) {
|
||||
return this.numberingService.getSequences(
|
||||
projectId ? Number(projectId) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@Patch('counters/:id')
|
||||
@ApiOperation({ summary: 'Update counter sequence value (Admin only)' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async updateCounter(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body('sequence') sequence: number
|
||||
) {
|
||||
return this.numberingService.setCounterValue(id, sequence);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Preview / Test
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('preview')
|
||||
@ApiOperation({ summary: 'Preview what a document number would look like' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Preview result without incrementing counter',
|
||||
})
|
||||
@RequirePermission('correspondence.read')
|
||||
async previewNumber(@Body() dto: PreviewNumberDto) {
|
||||
return this.numberingService.previewNumber({
|
||||
projectId: dto.projectId,
|
||||
originatorOrganizationId: dto.originatorOrganizationId,
|
||||
typeId: dto.correspondenceTypeId,
|
||||
subTypeId: dto.subTypeId,
|
||||
rfaTypeId: dto.rfaTypeId,
|
||||
disciplineId: dto.disciplineId,
|
||||
recipientOrganizationId: dto.recipientOrganizationId,
|
||||
year: dto.year,
|
||||
customTokens: dto.customTokens,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,36 +3,27 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { DocumentNumberingService } from './services/document-numbering.service';
|
||||
import { DocumentNumberingController } from './controllers/document-numbering.controller';
|
||||
import { DocumentNumberingAdminController } from './controllers/document-numbering-admin.controller';
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||
import { DocumentNumberReservation } from './entities/document-number-reservation.entity';
|
||||
import { DocumentNumberAudit } from './entities/document-number-audit.entity';
|
||||
import { DocumentNumberError } from './entities/document-number-error.entity';
|
||||
import { CounterService } from './services/counter.service';
|
||||
import { ReservationService } from './services/reservation.service';
|
||||
import { FormatService } from './services/format.service';
|
||||
import { DocumentNumberAudit } from './entities/document-number-audit.entity'; // [P0-4]
|
||||
import { DocumentNumberError } from './entities/document-number-error.entity'; // [P0-4]
|
||||
|
||||
// Master Entities ที่ต้องใช้ Lookup
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { Organization } from '../project/entities/organization.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
UserModule,
|
||||
TypeOrmModule.forFeature([
|
||||
DocumentNumberFormat,
|
||||
DocumentNumberCounter,
|
||||
DocumentNumberReservation,
|
||||
DocumentNumberAudit,
|
||||
DocumentNumberError,
|
||||
DocumentNumberAudit, // [P0-4]
|
||||
DocumentNumberError, // [P0-4]
|
||||
Project,
|
||||
Organization,
|
||||
CorrespondenceType,
|
||||
@@ -40,18 +31,7 @@ import { UserModule } from '../user/user.module';
|
||||
CorrespondenceSubType,
|
||||
]),
|
||||
],
|
||||
controllers: [DocumentNumberingController, DocumentNumberingAdminController],
|
||||
providers: [
|
||||
DocumentNumberingService,
|
||||
CounterService,
|
||||
ReservationService,
|
||||
FormatService,
|
||||
],
|
||||
exports: [
|
||||
DocumentNumberingService,
|
||||
CounterService,
|
||||
ReservationService,
|
||||
FormatService,
|
||||
],
|
||||
providers: [DocumentNumberingService],
|
||||
exports: [DocumentNumberingService],
|
||||
})
|
||||
export class DocumentNumberingModule {}
|
||||
|
||||
@@ -1,139 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DocumentNumberingService } from './services/document-numbering.service';
|
||||
import { CounterService } from './services/counter.service';
|
||||
import { ReservationService } from './services/reservation.service';
|
||||
import { FormatService } from './services/format.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { DocumentNumberAudit } from './entities/document-number-audit.entity';
|
||||
import { DocumentNumberError } from './entities/document-number-error.entity';
|
||||
|
||||
// Mock Redis and Redlock (legacy mocks, kept just in case)
|
||||
const mockRedis = {
|
||||
disconnect: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
const mockRedlock = {
|
||||
acquire: jest.fn(),
|
||||
};
|
||||
const mockLock = {
|
||||
release: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
jest.mock('ioredis', () => {
|
||||
return jest.fn().mockImplementation(() => mockRedis);
|
||||
});
|
||||
jest.mock('redlock', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return mockRedlock;
|
||||
});
|
||||
});
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
|
||||
describe('DocumentNumberingService', () => {
|
||||
let service: DocumentNumberingService;
|
||||
let module: TestingModule;
|
||||
let counterService: CounterService;
|
||||
let formatService: FormatService;
|
||||
|
||||
const mockContext = {
|
||||
projectId: 1,
|
||||
originatorOrganizationId: 1,
|
||||
typeId: 1,
|
||||
disciplineId: 1,
|
||||
year: 2025,
|
||||
customTokens: { TYPE_CODE: 'COR', ORG_CODE: 'GGL' },
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRedlock.acquire.mockResolvedValue(mockLock);
|
||||
|
||||
module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DocumentNumberingService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: { get: jest.fn().mockReturnValue('localhost') },
|
||||
},
|
||||
{
|
||||
provide: CounterService,
|
||||
useValue: {
|
||||
incrementCounter: jest.fn().mockResolvedValue(1),
|
||||
getCurrentSequence: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ReservationService,
|
||||
useValue: {
|
||||
reserve: jest.fn(),
|
||||
confirm: jest.fn(),
|
||||
cancel: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FormatService,
|
||||
useValue: {
|
||||
format: jest.fn().mockResolvedValue('0001'),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(DocumentNumberFormat),
|
||||
useValue: { findOne: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(DocumentNumberAudit),
|
||||
useValue: {
|
||||
create: jest.fn().mockReturnValue({ id: 1 }),
|
||||
save: jest.fn().mockResolvedValue({ id: 1 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(DocumentNumberError),
|
||||
useValue: {
|
||||
create: jest.fn().mockReturnValue({}),
|
||||
save: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
],
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [DocumentNumberingService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DocumentNumberingService>(DocumentNumberingService);
|
||||
counterService = module.get<CounterService>(CounterService);
|
||||
formatService = module.get<FormatService>(FormatService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('generateNextNumber', () => {
|
||||
it('should generate a new number successfully', async () => {
|
||||
(counterService.incrementCounter as jest.Mock).mockResolvedValue(1);
|
||||
(formatService.format as jest.Mock).mockResolvedValue('DOC-0001');
|
||||
|
||||
const result = await service.generateNextNumber(mockContext);
|
||||
|
||||
// Service returns object with number and auditId
|
||||
expect(result).toHaveProperty('number');
|
||||
expect(result).toHaveProperty('auditId');
|
||||
expect(result.number).toBe('DOC-0001');
|
||||
expect(counterService.incrementCounter).toHaveBeenCalled();
|
||||
expect(formatService.format).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when increment fails', async () => {
|
||||
// Mock CounterService to throw error
|
||||
(counterService.incrementCounter as jest.Mock).mockRejectedValue(
|
||||
new Error('Transaction failed')
|
||||
);
|
||||
|
||||
await expect(service.generateNextNumber(mockContext)).rejects.toThrow(
|
||||
'Transaction failed'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
// File: src/modules/document-numbering/document-numbering.service.ts
|
||||
import {
|
||||
Injectable,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
Repository,
|
||||
EntityManager,
|
||||
OptimisticLockVersionMismatchError,
|
||||
} from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
import Redlock from 'redlock';
|
||||
|
||||
// Entities
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { Project } from '../project/entities/project.entity'; // สมมติ path
|
||||
import { Organization } from '../project/entities/organization.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||
import { DocumentNumberAudit } from './entities/document-number-audit.entity'; // [P0-4]
|
||||
import { DocumentNumberError } from './entities/document-number-error.entity'; // [P0-4]
|
||||
|
||||
// Interfaces
|
||||
import {
|
||||
GenerateNumberContext,
|
||||
DecodedTokens,
|
||||
} from './interfaces/document-numbering.interface.js';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(DocumentNumberingService.name);
|
||||
private redisClient!: Redis;
|
||||
private redlock!: Redlock;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(DocumentNumberCounter)
|
||||
private counterRepo: Repository<DocumentNumberCounter>,
|
||||
@InjectRepository(DocumentNumberFormat)
|
||||
private formatRepo: Repository<DocumentNumberFormat>,
|
||||
|
||||
// Inject Repositories สำหรับดึง Code มาทำ Token Replacement
|
||||
@InjectRepository(Project) private projectRepo: Repository<Project>,
|
||||
@InjectRepository(Organization) private orgRepo: Repository<Organization>,
|
||||
@InjectRepository(CorrespondenceType)
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(Discipline)
|
||||
private disciplineRepo: Repository<Discipline>,
|
||||
@InjectRepository(CorrespondenceSubType)
|
||||
private subTypeRepo: Repository<CorrespondenceSubType>,
|
||||
@InjectRepository(DocumentNumberAudit) // [P0-4]
|
||||
private auditRepo: Repository<DocumentNumberAudit>,
|
||||
@InjectRepository(DocumentNumberError) // [P0-4]
|
||||
private errorRepo: Repository<DocumentNumberError>,
|
||||
|
||||
private configService: ConfigService
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
// 1. Setup Redis Connection & Redlock
|
||||
const host = this.configService.get<string>('REDIS_HOST', 'localhost');
|
||||
const port = this.configService.get<number>('REDIS_PORT', 6379);
|
||||
const password = this.configService.get<string>('REDIS_PASSWORD');
|
||||
|
||||
this.redisClient = new Redis({ host, port, password });
|
||||
|
||||
// Config Redlock สำหรับ Distributed Lock
|
||||
this.redlock = new Redlock([this.redisClient], {
|
||||
driftFactor: 0.01,
|
||||
retryCount: 10, // Retry 10 ครั้ง
|
||||
retryDelay: 200, // รอ 200ms ต่อครั้ง
|
||||
retryJitter: 200,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Document Numbering Service initialized (Redis: ${host}:${port})`
|
||||
);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
this.redisClient.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้างเลขที่เอกสารใหม่ (Thread-Safe & Gap-free)
|
||||
*/
|
||||
async generateNextNumber(ctx: GenerateNumberContext): Promise<string> {
|
||||
const year = ctx.year || new Date().getFullYear();
|
||||
const disciplineId = ctx.disciplineId || 0;
|
||||
|
||||
// 1. ดึงข้อมูล Master Data มาเตรียมไว้ (Tokens) นอก Lock เพื่อ Performance
|
||||
const tokens = await this.resolveTokens(ctx, year);
|
||||
|
||||
// 2. ดึง Format Template
|
||||
const formatTemplate = await this.getFormatTemplate(
|
||||
ctx.projectId,
|
||||
ctx.typeId
|
||||
);
|
||||
|
||||
// 3. สร้าง Resource Key สำหรับ Lock (ละเอียดถึงระดับ Discipline)
|
||||
// Key: doc_num:{projectId}:{typeId}:{disciplineId}:{year}
|
||||
const resourceKey = `doc_num:${ctx.projectId}:${ctx.typeId}:${disciplineId}:${year}`;
|
||||
const lockTtl = 5000; // 5 วินาที
|
||||
|
||||
let lock;
|
||||
try {
|
||||
// 🔒 LAYER 1: Acquire Redis Lock
|
||||
lock = await this.redlock.acquire([resourceKey], lockTtl);
|
||||
|
||||
// 🔄 LAYER 2: Optimistic Lock Loop
|
||||
const maxRetries = 3;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
// A. ดึง Counter ปัจจุบัน
|
||||
let counter = await this.counterRepo.findOne({
|
||||
where: {
|
||||
projectId: ctx.projectId,
|
||||
originatorId: ctx.originatorId,
|
||||
typeId: ctx.typeId,
|
||||
disciplineId: disciplineId,
|
||||
year: year,
|
||||
},
|
||||
});
|
||||
|
||||
// B. ถ้ายังไม่มี ให้เริ่มใหม่ที่ 0
|
||||
if (!counter) {
|
||||
counter = this.counterRepo.create({
|
||||
projectId: ctx.projectId,
|
||||
originatorId: ctx.originatorId,
|
||||
typeId: ctx.typeId,
|
||||
disciplineId: disciplineId,
|
||||
year: year,
|
||||
lastNumber: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// C. Increment Sequence
|
||||
counter.lastNumber += 1;
|
||||
|
||||
// D. Save (TypeORM จะเช็ค version column ตรงนี้)
|
||||
await this.counterRepo.save(counter);
|
||||
|
||||
// E. Format Result
|
||||
const generatedNumber = this.replaceTokens(
|
||||
formatTemplate,
|
||||
tokens,
|
||||
counter.lastNumber
|
||||
);
|
||||
|
||||
// [P0-4] F. Audit Logging
|
||||
await this.logAudit({
|
||||
generatedNumber,
|
||||
counterKey: resourceKey,
|
||||
templateUsed: formatTemplate,
|
||||
sequenceNumber: counter.lastNumber,
|
||||
userId: ctx.userId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
retryCount: i,
|
||||
lockWaitMs: 0, // TODO: calculate actual wait time
|
||||
});
|
||||
|
||||
return generatedNumber;
|
||||
} catch (err) {
|
||||
// ถ้า Version ไม่ตรง (มีคนแทรกได้ในเสี้ยววินาที) ให้ Retry
|
||||
if (err instanceof OptimisticLockVersionMismatchError) {
|
||||
this.logger.warn(
|
||||
`Optimistic Lock Collision for ${resourceKey}. Retrying...`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
'Failed to generate document number after retries.'
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error generating number for ${resourceKey}`, error);
|
||||
|
||||
// [P0-4] Log error
|
||||
await this.logError({
|
||||
counterKey: resourceKey,
|
||||
errorType: this.classifyError(error),
|
||||
errorMessage: error.message,
|
||||
stackTrace: error.stack,
|
||||
userId: ctx.userId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
context: ctx,
|
||||
}).catch(() => {}); // Don't throw if error logging fails
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// 🔓 Release Lock
|
||||
if (lock) {
|
||||
await lock.release().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: ดึงข้อมูล Code ต่างๆ จาก ID เพื่อนำมาแทนที่ใน Template
|
||||
*/
|
||||
private async resolveTokens(
|
||||
ctx: GenerateNumberContext,
|
||||
year: number
|
||||
): Promise<DecodedTokens> {
|
||||
const [project, org, type] = await Promise.all([
|
||||
this.projectRepo.findOne({ where: { id: ctx.projectId } }),
|
||||
this.orgRepo.findOne({ where: { id: ctx.originatorId } }),
|
||||
this.typeRepo.findOne({ where: { id: ctx.typeId } }),
|
||||
]);
|
||||
|
||||
if (!project || !org || !type) {
|
||||
throw new NotFoundException('Project, Organization, or Type not found');
|
||||
}
|
||||
|
||||
let disciplineCode = '000';
|
||||
if (ctx.disciplineId) {
|
||||
const discipline = await this.disciplineRepo.findOne({
|
||||
where: { id: ctx.disciplineId },
|
||||
});
|
||||
if (discipline) disciplineCode = discipline.disciplineCode;
|
||||
}
|
||||
|
||||
let subTypeCode = '00';
|
||||
let subTypeNumber = '00';
|
||||
if (ctx.subTypeId) {
|
||||
const subType = await this.subTypeRepo.findOne({
|
||||
where: { id: ctx.subTypeId },
|
||||
});
|
||||
if (subType) {
|
||||
subTypeCode = subType.subTypeCode;
|
||||
subTypeNumber = subType.subTypeNumber || '00';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Christian Year to Buddhist Year if needed (Req usually uses Christian, but prepared logic)
|
||||
// ใน Req 6B ตัวอย่างใช้ 2568 (พ.ศ.) ดังนั้นต้องแปลง
|
||||
const yearTh = (year + 543).toString();
|
||||
|
||||
// [P1-4] Resolve recipient organization
|
||||
let recipientCode = '';
|
||||
if (ctx.recipientOrgId) {
|
||||
const recipient = await this.orgRepo.findOne({
|
||||
where: { id: ctx.recipientOrgId },
|
||||
});
|
||||
if (recipient) {
|
||||
recipientCode = recipient.organizationCode;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectCode: project.projectCode,
|
||||
orgCode: org.organizationCode,
|
||||
typeCode: type.typeCode,
|
||||
disciplineCode,
|
||||
subTypeCode,
|
||||
subTypeNumber,
|
||||
year: yearTh,
|
||||
yearShort: yearTh.slice(-2), // 68
|
||||
recipientCode, // [P1-4]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: หา Template จาก DB หรือใช้ Default
|
||||
*/
|
||||
private async getFormatTemplate(
|
||||
projectId: number,
|
||||
typeId: number
|
||||
): Promise<string> {
|
||||
const format = await this.formatRepo.findOne({
|
||||
where: { projectId, correspondenceTypeId: typeId },
|
||||
});
|
||||
// Default Fallback Format (ตาม Req 2.1)
|
||||
return format ? format.formatTemplate : '{ORG}-{ORG}-{SEQ:4}-{YEAR}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: แทนที่ Token ใน Template ด้วยค่าจริง
|
||||
*/
|
||||
private replaceTokens(
|
||||
template: string,
|
||||
tokens: DecodedTokens,
|
||||
seq: number
|
||||
): string {
|
||||
let result = template;
|
||||
|
||||
const replacements: Record<string, string> = {
|
||||
'{PROJECT}': tokens.projectCode,
|
||||
'{ORG}': tokens.orgCode,
|
||||
'{TYPE}': tokens.typeCode,
|
||||
'{DISCIPLINE}': tokens.disciplineCode,
|
||||
'{SUBTYPE}': tokens.subTypeCode,
|
||||
'{SUBTYPE_NUM}': tokens.subTypeNumber, // [Req 6B] For Transmittal/RFA
|
||||
'{RECIPIENT}': tokens.recipientCode, // [P1-4] Recipient organization
|
||||
'{YEAR}': tokens.year,
|
||||
'{YEAR_SHORT}': tokens.yearShort,
|
||||
};
|
||||
|
||||
// 1. Replace Standard Tokens
|
||||
for (const [key, value] of Object.entries(replacements)) {
|
||||
// ใช้ Global Replace
|
||||
result = result.split(key).join(value);
|
||||
}
|
||||
|
||||
// 2. Replace Sequence Token {SEQ:n} e.g., {SEQ:4} -> 0001
|
||||
result = result.replace(/{SEQ(?::(\d+))?}/g, (_, digits) => {
|
||||
const padLength = digits ? parseInt(digits, 10) : 4; // Default padding 4
|
||||
return seq.toString().padStart(padLength, '0');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* [P0-4] Log successful number generation to audit table
|
||||
*/
|
||||
private async logAudit(
|
||||
auditData: Partial<DocumentNumberAudit>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.auditRepo.save(auditData);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log audit', error);
|
||||
// Don't throw - audit failure shouldn't block number generation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [P0-4] Log error to error table
|
||||
*/
|
||||
private async logError(
|
||||
errorData: Partial<DocumentNumberError>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.errorRepo.save(errorData);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to log error', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [P0-4] Classify error type for logging
|
||||
*/
|
||||
private classifyError(error: any): string {
|
||||
if (error.message?.includes('lock') || error.message?.includes('Lock')) {
|
||||
return 'LOCK_TIMEOUT';
|
||||
}
|
||||
if (error instanceof OptimisticLockVersionMismatchError) {
|
||||
return 'VERSION_CONFLICT';
|
||||
}
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
||||
return 'REDIS_ERROR';
|
||||
}
|
||||
if (error.name === 'QueryFailedError') {
|
||||
return 'DB_ERROR';
|
||||
}
|
||||
return 'VALIDATION_ERROR';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { IsString, IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class ConfirmReservationDto {
|
||||
@IsString()
|
||||
token!: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
documentId?: number;
|
||||
}
|
||||
|
||||
export class ConfirmReservationResponseDto {
|
||||
documentNumber!: string;
|
||||
confirmedAt!: Date;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user