Compare commits
21 Commits
d4e2f23f16
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83704377f4 | ||
|
|
aaa5da3ec1 | ||
|
|
48ed74a27b | ||
|
|
95ee94997f | ||
|
|
9c1e175b76 | ||
|
|
78370fb590 | ||
|
|
ec35521258 | ||
|
|
d964546c8d | ||
|
|
2473c4c474 | ||
|
|
3fa28bd14f | ||
|
|
c8a0f281ef | ||
|
|
aa96cd90e3 | ||
| 8aceced902 | |||
|
|
863a727756 | ||
| dcd126d704 | |||
| 32d820ea6b | |||
|
|
5c49bac772 | ||
|
|
be3b71007a | ||
|
|
fea8ed6b80 | ||
|
|
c5250f3e70 | ||
|
|
7e846bf045 |
@@ -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.
|
- *Crucial:* Check `specs/05-decisions/` (ADRs) to ensure you do not violate previously agreed-upon technical decisions.
|
||||||
|
|
||||||
5. **💾 DATABASE & SCHEMA (`specs/07-databasee/`)**
|
5. **💾 DATABASE & SCHEMA (`specs/07-databasee/`)**
|
||||||
- *Action:* - **Read `specs/07-database/lcbp3-v1.5.1-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints.
|
- *Action:* - **Read `specs/07-database/lcbp3-v1.6.0-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.
|
- **Consult `specs/07-database/data-dictionary-v1.6.0.md`** for field meanings and business rules.
|
||||||
- **Check `specs/database/lcbp3-v1.5.1-seed.sql`** to understand initial data states.
|
- **Check `specs/07-database/lcbp3-v1.6.0-seed.sql`** to understand initial data states.
|
||||||
- *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here.
|
- *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here.
|
||||||
|
|
||||||
6. **⚙️ IMPLEMENTATION DETAILS (`specs/03-implementation/`)**
|
6. **⚙️ IMPLEMENTATION DETAILS (`specs/03-implementation/`)**
|
||||||
|
|||||||
10
.aignore
Normal file
10
.aignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
backend/node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
backend/dist/
|
||||||
|
frontend/dist/
|
||||||
|
backend/build/
|
||||||
|
frontend/build/
|
||||||
|
docs/backup/
|
||||||
|
.git/
|
||||||
|
*.log
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
|
||||||
# NAP-DMS Project Context & Rules
|
# NAP-DMS Project Context & Rules
|
||||||
|
|
||||||
## 🧠 Role & Persona
|
## 🧠 Role & Persona
|
||||||
@@ -14,8 +18,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
|||||||
|
|
||||||
## 💻 Tech Stack & Constraints
|
## 💻 Tech Stack & Constraints
|
||||||
|
|
||||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis (BullMQ).
|
- **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.
|
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, React Context / Zustand, React Hook Form + Zod, Axios.
|
||||||
- **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
|
- **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
|
||||||
|
|
||||||
## 🛡️ Security & Integrity Rules
|
## 🛡️ Security & Integrity Rules
|
||||||
@@ -27,8 +31,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
|||||||
|
|
||||||
## workflow Guidelines
|
## workflow Guidelines
|
||||||
|
|
||||||
- When implementing **Workflow Engine**, strictly follow the **DSL** design in `2_Backend_Plan_V1_4_4.Phase6A.md`.
|
- When implementing strictly follow the documents in `specs/`.
|
||||||
- Always verify database schema against `4_Data_Dictionary_V1_4_4.md` before writing queries.
|
- Always verify database schema against `specs/07-database/` before writing queries.
|
||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
backend/node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
*.min.js
|
*.min.js
|
||||||
|
|||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -33,6 +33,8 @@
|
|||||||
"alefragnani.bookmarks",
|
"alefragnani.bookmarks",
|
||||||
"pkief.material-icon-theme",
|
"pkief.material-icon-theme",
|
||||||
"github.copilot",
|
"github.copilot",
|
||||||
"bierner.markdown-mermaid"
|
"bierner.markdown-mermaid",
|
||||||
|
"vitest.explorer",
|
||||||
|
"google.geminicodeassist"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
38
2git.ps1
38
2git.ps1
@@ -1,14 +1,38 @@
|
|||||||
param([string]$Message = "Backup")
|
param([string]$Message = "Backup")
|
||||||
|
|
||||||
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
$Timestamp = Get-Date -Format "yyMMdd:HHmm"
|
||||||
$CommitMsg = "Backup: $Message | $Timestamp"
|
$CommitMsg = "$Timestamp $Message"
|
||||||
|
|
||||||
Write-Host "Backup: $CommitMsg" -ForegroundColor Cyan
|
Write-Host "📦 $CommitMsg" -ForegroundColor Cyan
|
||||||
|
|
||||||
git add .
|
git add .
|
||||||
git commit -m $CommitMsg
|
|
||||||
git push origin main
|
|
||||||
git push github main
|
|
||||||
|
|
||||||
Write-Host "Done!" -ForegroundColor Green
|
# 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
|
||||||
pause
|
pause
|
||||||
|
|||||||
185
CHANGELOG.md
185
CHANGELOG.md
@@ -1,16 +1,191 @@
|
|||||||
# Version history
|
# Version History
|
||||||
|
|
||||||
## 1.4.5 (2025-11-28)
|
## [Unreleased]
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- Backend Document Numbering Refactor (TASK-BE-017)
|
||||||
|
- E2E Testing & UAT preparation
|
||||||
|
- Production deployment preparation
|
||||||
|
|
||||||
|
## 1.7.0 (2025-12-18)
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
**Schema Stabilization & Document Numbering Overhaul** - Significant schema updates to support advanced document numbering (reservations, varying reset scopes) and a unified workflow engine.
|
||||||
|
|
||||||
- Backend development 80% remaining test tasks
|
### 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/`
|
||||||
|
|
||||||
## 1.5.0 (2025-11-30)
|
## 1.5.0 (2025-11-30)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Initial spec-kit structure establishment and documentation organization.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Changed the version to 1.5.0
|
- Changed the version to 1.5.0
|
||||||
- Modified to Spec-kit
|
- Modified to Spec-kit
|
||||||
|
|
||||||
### Summary
|
|
||||||
|
|||||||
@@ -20,18 +20,19 @@
|
|||||||
|
|
||||||
## 🗂️ Specification Structure
|
## 🗂️ Specification Structure
|
||||||
|
|
||||||
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 6 หมวดหลัก:
|
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 9 หมวดหลัก:
|
||||||
|
|
||||||
```
|
```
|
||||||
specs/
|
specs/
|
||||||
├── 00-overview/ # ภาพรวมโครงการ
|
├── 00-overview/ # ภาพรวมโครงการ (3 docs)
|
||||||
│ ├── README.md # Project overview
|
│ ├── README.md # Project overview
|
||||||
│ └── glossary.md # คำศัพท์เทคนิค
|
│ ├── glossary.md # คำศัพท์เทคนิค
|
||||||
|
│ └── quick-start.md # Quick start guide
|
||||||
│
|
│
|
||||||
├── 01-requirements/ # ข้อกำหนดระบบ
|
├── 01-requirements/ # ข้อกำหนดระบบ (21 docs)
|
||||||
│ ├── README.md # Requirements overview
|
│ ├── README.md # Requirements overview
|
||||||
│ ├── 01-objectives.md # วัตถุประสงค์
|
│ ├── 01-objectives.md # วัตถุประสงค์
|
||||||
│ ├── 02-architecture.md # สถาปัตยกรรม
|
│ ├── 02-architecture.md # สถาปัตยกรรม
|
||||||
│ ├── 03-functional-requirements.md
|
│ ├── 03-functional-requirements.md
|
||||||
│ ├── 03.1-project-management.md
|
│ ├── 03.1-project-management.md
|
||||||
│ ├── 03.2-correspondence.md
|
│ ├── 03.2-correspondence.md
|
||||||
@@ -50,39 +51,59 @@ specs/
|
|||||||
│ ├── 06-non-functional.md
|
│ ├── 06-non-functional.md
|
||||||
│ └── 07-testing.md
|
│ └── 07-testing.md
|
||||||
│
|
│
|
||||||
├── 02-architecture/ # สถาปัตยกรรมระบบ
|
├── 02-architecture/ # สถาปัตยกรรมระบบ (4 docs)
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ ├── system-architecture.md
|
│ ├── system-architecture.md
|
||||||
│ ├── api-design.md
|
│ ├── api-design.md
|
||||||
│ └── data-model.md
|
│ └── data-model.md
|
||||||
│
|
│
|
||||||
├── 03-implementation/ # แผนการพัฒนา
|
├── 03-implementation/ # แผนการพัฒนา (5 docs)
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ ├── backend-plan.md
|
│ ├── backend-guidelines.md
|
||||||
│ ├── frontend-plan.md
|
│ ├── frontend-guidelines.md
|
||||||
│ └── integration-plan.md
|
│ ├── testing-strategy.md
|
||||||
|
│ └── code-standards.md
|
||||||
│
|
│
|
||||||
├── 04-operations/ # การดำเนินงาน
|
├── 04-operations/ # การดำเนินงาน (9 docs)
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ ├── deployment.md
|
│ ├── deployment.md
|
||||||
│ └── monitoring.md
|
│ ├── monitoring.md
|
||||||
|
│ └── ...
|
||||||
│
|
│
|
||||||
└── 05-decisions/ # Architecture Decision Records
|
├── 05-decisions/ # Architecture Decision Records (17 ADRs)
|
||||||
├── README.md
|
│ ├── README.md
|
||||||
├── 001-workflow-engine.md
|
│ ├── ADR-001-workflow-engine.md
|
||||||
└── 002-file-storage.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)
|
||||||
|
└── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📋 หมวดหมู่เอกสาร
|
### 📋 หมวดหมู่เอกสาร
|
||||||
|
|
||||||
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
||||||
| --------------------- | ----------------------------- | ----------------------------- |
|
| --------------------- | ----------------------------- | ----------------------------- |
|
||||||
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
||||||
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
||||||
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
||||||
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
||||||
| **04-operations** | Deployment และ Operations | DevOps Team |
|
| **04-operations** | Deployment และ Operations | DevOps Team |
|
||||||
| **05-decisions** | Architecture Decision Records | Tech Lead + Senior Developers |
|
| **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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -454,11 +475,11 @@ Then [expected result]
|
|||||||
|
|
||||||
### Review Levels
|
### Review Levels
|
||||||
|
|
||||||
| Level | Reviewer | Scope |
|
| Level | Reviewer | Scope |
|
||||||
|-------|----------|-------|
|
| ------------------------ | --------------- | ------------------------------- |
|
||||||
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
||||||
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
||||||
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
||||||
|
|
||||||
### Review Timeline
|
### Review Timeline
|
||||||
|
|
||||||
|
|||||||
196
README.md
196
README.md
@@ -4,9 +4,23 @@
|
|||||||
>
|
>
|
||||||
> ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
> ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,7 +55,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
|||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
"framework": "NestJS (TypeScript, ESM)",
|
"framework": "NestJS (TypeScript, ESM)",
|
||||||
"database": "MariaDB 10.11",
|
"database": "MariaDB 11.8",
|
||||||
"orm": "TypeORM",
|
"orm": "TypeORM",
|
||||||
"authentication": "JWT + Passport",
|
"authentication": "JWT + Passport",
|
||||||
"authorization": "CASL (RBAC)",
|
"authorization": "CASL (RBAC)",
|
||||||
@@ -111,7 +125,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
|||||||
- **Node.js**: v20.x หรือสูงกว่า
|
- **Node.js**: v20.x หรือสูงกว่า
|
||||||
- **pnpm**: v8.x หรือสูงกว่า
|
- **pnpm**: v8.x หรือสูงกว่า
|
||||||
- **Docker**: v24.x หรือสูงกว่า
|
- **Docker**: v24.x หรือสูงกว่า
|
||||||
- **MariaDB**: 10.11
|
- **MariaDB**: 11.8
|
||||||
- **Redis**: 7.x
|
- **Redis**: 7.x
|
||||||
|
|
||||||
### การติดตั้ง
|
### การติดตั้ง
|
||||||
@@ -194,46 +208,88 @@ Superadmin:
|
|||||||
|
|
||||||
```
|
```
|
||||||
lcbp3-dms/
|
lcbp3-dms/
|
||||||
├── backend/ # NestJS Backend
|
├── backend/ # 🔧 NestJS Backend
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── common/ # Shared modules
|
│ │ ├── common/ # Shared utilities, guards, decorators
|
||||||
│ │ ├── modules/ # Feature modules
|
│ │ ├── config/ # Configuration module
|
||||||
│ │ │ ├── auth/
|
│ │ ├── database/ # Database entities & migrations
|
||||||
│ │ │ ├── user/
|
│ │ ├── modules/ # Feature modules (17 modules)
|
||||||
│ │ │ ├── project/
|
│ │ │ ├── auth/ # JWT Authentication
|
||||||
│ │ │ ├── correspondence/
|
│ │ │ ├── user/ # User management & RBAC
|
||||||
│ │ │ ├── rfa/
|
│ │ │ ├── project/ # Project & Contract management
|
||||||
│ │ │ ├── drawing/
|
│ │ │ ├── correspondence/ # Correspondence module
|
||||||
│ │ │ ├── workflow-engine/
|
│ │ │ ├── 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
|
│ │ └── main.ts
|
||||||
│ ├── test/
|
│ ├── test/ # Unit & E2E tests
|
||||||
│ └── package.json
|
│ └── package.json
|
||||||
│
|
│
|
||||||
├── frontend/ # Next.js Frontend
|
├── frontend/ # 🎨 Next.js Frontend
|
||||||
│ ├── app/ # App Router
|
│ ├── app/ # App Router
|
||||||
│ ├── components/ # React Components
|
│ │ ├── (admin)/ # Admin panel routes
|
||||||
│ ├── lib/ # Utilities
|
│ │ │ └── admin/
|
||||||
|
│ │ │ ├── workflows/ # Workflow configuration
|
||||||
|
│ │ │ ├── numbering/ # Document numbering config
|
||||||
|
│ │ │ ├── users/ # User management
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── (auth)/ # Authentication pages
|
||||||
|
│ │ ├── (dashboard)/ # Main dashboard routes
|
||||||
|
│ │ │ ├── correspondences/
|
||||||
|
│ │ │ ├── rfas/
|
||||||
|
│ │ │ ├── drawings/
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── 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
|
||||||
│ └── package.json
|
│ └── package.json
|
||||||
│
|
│
|
||||||
├── docs/ # 📚 Legacy documentation
|
├── 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
|
||||||
│
|
│
|
||||||
├── specs/ # 📘 Project Specifications (v1.5.1)
|
├── docs/ # 📚 Legacy documentation
|
||||||
│ ├── 00-overview/ # Project overview & glossary
|
├── diagrams/ # 📊 Architecture diagrams
|
||||||
│ ├── 01-requirements/ # Functional requirements
|
├── infrastructure/ # 🐳 Docker & Deployment configs
|
||||||
│ ├── 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
|
├── .gemini/ # 🤖 AI agent configuration
|
||||||
│ └── Markdown/ # Legacy docs
|
├── .agent/ # Agent workflows
|
||||||
│
|
├── GEMINI.md # AI coding guidelines
|
||||||
└── pnpm-workspace.yaml
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
├── CHANGELOG.md # Version history
|
||||||
|
└── pnpm-workspace.yaml # Monorepo configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -248,16 +304,16 @@ lcbp3-dms/
|
|||||||
| **Requirements** | ข้อกำหนดระบบและฟังก์ชันการทำงาน | `specs/01-requirements/` |
|
| **Requirements** | ข้อกำหนดระบบและฟังก์ชันการทำงาน | `specs/01-requirements/` |
|
||||||
| **Architecture** | สถาปัตยกรรมระบบ, ADRs | `specs/02-architecture/` |
|
| **Architecture** | สถาปัตยกรรมระบบ, ADRs | `specs/02-architecture/` |
|
||||||
| **Implementation** | แนวทางการพัฒนา Backend/Frontend | `specs/03-implementation/` |
|
| **Implementation** | แนวทางการพัฒนา Backend/Frontend | `specs/03-implementation/` |
|
||||||
| **Database** | Schema v1.5.1 + Seed Data | `specs/07-database/` |
|
| **Database** | Schema v1.7.0 + Seed Data | `specs/07-database/` |
|
||||||
|
|
||||||
### Schema & Seed Data
|
### Schema & Seed Data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Import schema
|
# Import schema
|
||||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-schema.sql
|
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.7.0-schema.sql
|
||||||
|
|
||||||
# Import seed data
|
# Import seed data
|
||||||
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-seed.sql
|
mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.7.0-seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### Legacy Documentation
|
### Legacy Documentation
|
||||||
@@ -466,7 +522,7 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
|||||||
|
|
||||||
สำหรับคำถามหรือปัญหา กรุณาติดต่อ:
|
สำหรับคำถามหรือปัญหา กรุณาติดต่อ:
|
||||||
|
|
||||||
- **Email**: support@np-dms.work
|
- **Email**: <support@np-dms.work>
|
||||||
- **Internal Chat**: [ระบุช่องทาง]
|
- **Internal Chat**: [ระบุช่องทาง]
|
||||||
- **Issue Tracker**: [Gitea Issues](https://git.np-dms.work/lcbp3/lcbp3-dms/issues)
|
- **Issue Tracker**: [Gitea Issues](https://git.np-dms.work/lcbp3/lcbp3-dms/issues)
|
||||||
|
|
||||||
@@ -474,26 +530,56 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
|||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
### Version 1.5.1 (Current - Dec 2025)
|
### Version 1.5.1 (Current - Dec 2025) ✅ **FEATURE COMPLETE**
|
||||||
|
|
||||||
- ✅ Core Infrastructure
|
**Backend (18 Modules - ~95%)**
|
||||||
- ✅ Authentication & Authorization (JWT + CASL RBAC)
|
- ✅ Core Infrastructure (Auth, RBAC, File Storage)
|
||||||
- ✅ **CASL RBAC 4-Level** - Global, Org, Project, Contract
|
- ✅ Authentication & Authorization (JWT + CASL RBAC 4-Level)
|
||||||
- ✅ **Workflow DSL Parser** - Zod validation & state machine
|
|
||||||
- ✅ Correspondence Module (Master-Revision Pattern)
|
- ✅ Correspondence Module (Master-Revision Pattern)
|
||||||
- ✅ **Document Number Audit** - Compliance tracking
|
- ✅ RFA Module (Full CRUD + Workflow)
|
||||||
- ✅ **All Token Types** - Including {RECIPIENT}
|
- ✅ Drawing Module (Contract + Shop Drawings)
|
||||||
- 🔄 RFA Module
|
- ✅ Workflow Engine (DSL-based Hybrid)
|
||||||
- 🔄 Drawing Module
|
- ✅ Document Numbering (Redlock + Optimistic Locking)
|
||||||
|
- ✅ Search (Elasticsearch Direct Indexing)
|
||||||
|
- ✅ Transmittal & Circulation Modules
|
||||||
|
- ✅ Notification & Audit Systems
|
||||||
|
- ✅ Master Data Management
|
||||||
|
- ✅ User Management
|
||||||
|
- ✅ Dashboard & Monitoring
|
||||||
- ✅ Swagger API Documentation
|
- ✅ Swagger API Documentation
|
||||||
|
|
||||||
### Version 1.6.0 (Planned)
|
**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)
|
||||||
|
|
||||||
- 📋 Advanced Reporting
|
**Documentation**
|
||||||
- 📊 Dashboard Analytics
|
- ✅ Complete specs/ v1.6.0 (21 requirements, 17 ADRs)
|
||||||
- 🔔 Enhanced Notifications (LINE/Email)
|
- ✅ Database Schema v1.6.0 with seed data
|
||||||
- 🔄 E2E Tests for Critical APIs
|
- ✅ Implementation & Operations Guides
|
||||||
- 📈 Prometheus Metrics
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
72
backend/build-output.txt
Normal file
72
backend/build-output.txt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
> 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).
|
||||||
|
|
||||||
141
backend/build_log.txt
Normal file
141
backend/build_log.txt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
> 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).
|
||||||
|
|
||||||
1416
backend/doc-output.txt
Normal file
1416
backend/doc-output.txt
Normal file
File diff suppressed because it is too large
Load Diff
30
backend/docker-compose.test.yml
Normal file
30
backend/docker-compose.test.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
421
backend/e2e-output.txt
Normal file
421
backend/e2e-output.txt
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
109
backend/e2e-output10.txt
Normal file
109
backend/e2e-output10.txt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
100
backend/e2e-output11.txt
Normal file
100
backend/e2e-output11.txt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
100
backend/e2e-output12.txt
Normal file
100
backend/e2e-output12.txt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
100
backend/e2e-output13.txt
Normal file
100
backend/e2e-output13.txt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
84
backend/e2e-output14.txt
Normal file
84
backend/e2e-output14.txt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
84
backend/e2e-output15.txt
Normal file
84
backend/e2e-output15.txt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
63
backend/e2e-output2.txt
Normal file
63
backend/e2e-output2.txt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
165
backend/e2e-output3.txt
Normal file
165
backend/e2e-output3.txt
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
83
backend/e2e-output4.txt
Normal file
83
backend/e2e-output4.txt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
214
backend/e2e-output5.txt
Normal file
214
backend/e2e-output5.txt
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
220
backend/e2e-output6.txt
Normal file
220
backend/e2e-output6.txt
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
220
backend/e2e-output7.txt
Normal file
220
backend/e2e-output7.txt
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
111
backend/e2e-output8.txt
Normal file
111
backend/e2e-output8.txt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
111
backend/e2e-output9.txt
Normal file
111
backend/e2e-output9.txt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- 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,13 +7,15 @@
|
|||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
|
"doc": "npx @compodoc/compodoc -p tsconfig.doc.json -s",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest --forceExit",
|
||||||
|
"test:debug-handles": "jest --detectOpenHandles",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@nestjs/websockets": "^11.1.9",
|
"@nestjs/websockets": "^11.1.9",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
"@willsoto/nestjs-prometheus": "^6.0.2",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"async-retry": "^1.3.3",
|
"async-retry": "^1.3.3",
|
||||||
@@ -71,11 +74,12 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.18.3",
|
"winston": "^3.18.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@compodoc/compodoc": "^1.1.32",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
@@ -93,7 +97,7 @@
|
|||||||
"@types/opossum": "^8.1.9",
|
"@types/opossum": "^8.1.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^9.0.8",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
|||||||
9656
backend/pnpm-lock.yaml
generated
9656
backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
13
backend/rfa_test_output.txt
Normal file
13
backend/rfa_test_output.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
> 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.
|
||||||
31
backend/scripts/check-connection.ts
Normal file
31
backend/scripts/check-connection.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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,6 +29,8 @@ import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
|||||||
import { AuthModule } from './common/auth/auth.module.js';
|
import { AuthModule } from './common/auth/auth.module.js';
|
||||||
import { UserModule } from './modules/user/user.module';
|
import { UserModule } from './modules/user/user.module';
|
||||||
import { ProjectModule } from './modules/project/project.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 { MasterModule } from './modules/master/master.module'; // [NEW] ✅ เพิ่ม MasterModule
|
||||||
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
||||||
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
||||||
@@ -40,9 +42,11 @@ import { DrawingModule } from './modules/drawing/drawing.module';
|
|||||||
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
||||||
import { CirculationModule } from './modules/circulation/circulation.module';
|
import { CirculationModule } from './modules/circulation/circulation.module';
|
||||||
import { NotificationModule } from './modules/notification/notification.module';
|
import { NotificationModule } from './modules/notification/notification.module';
|
||||||
|
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||||
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
||||||
import { ResilienceModule } from './common/resilience/resilience.module';
|
import { ResilienceModule } from './common/resilience/resilience.module';
|
||||||
import { SearchModule } from './modules/search/search.module';
|
import { SearchModule } from './modules/search/search.module';
|
||||||
|
import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -88,7 +92,7 @@ import { SearchModule } from './modules/search/search.module';
|
|||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
type: 'mariadb',
|
type: 'mariadb',
|
||||||
host: configService.get<string>('DB_HOST'),
|
host: configService.get<string>('DB_HOST'),
|
||||||
port: configService.get<number>('DB_PORT'),
|
port: configService.get<number>('DB_PORT'),
|
||||||
@@ -107,7 +111,7 @@ import { SearchModule } from './modules/search/search.module';
|
|||||||
BullModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
connection: {
|
connection: {
|
||||||
host: configService.get<string>('REDIS_HOST'),
|
host: configService.get<string>('REDIS_HOST'),
|
||||||
port: configService.get<number>('REDIS_PORT'),
|
port: configService.get<number>('REDIS_PORT'),
|
||||||
@@ -136,7 +140,10 @@ import { SearchModule } from './modules/search/search.module';
|
|||||||
// 📦 Feature Modules
|
// 📦 Feature Modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
UserModule,
|
||||||
ProjectModule,
|
ProjectModule,
|
||||||
|
OrganizationModule,
|
||||||
|
ContractModule,
|
||||||
MasterModule, // ✅ [NEW] Register MasterModule here
|
MasterModule, // ✅ [NEW] Register MasterModule here
|
||||||
FileStorageModule,
|
FileStorageModule,
|
||||||
DocumentNumberingModule,
|
DocumentNumberingModule,
|
||||||
@@ -149,6 +156,8 @@ import { SearchModule } from './modules/search/search.module';
|
|||||||
CirculationModule,
|
CirculationModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
|
DashboardModule,
|
||||||
|
AuditLogModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,30 +1,86 @@
|
|||||||
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AuthService } from './auth.service.js';
|
import { UnauthorizedException } from '@nestjs/common';
|
||||||
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
|
import { AuthController } from './auth.controller';
|
||||||
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Controller('auth')
|
describe('AuthController', () => {
|
||||||
export class AuthController {
|
let controller: AuthController;
|
||||||
constructor(private authService: AuthService) {}
|
let mockAuthService: Partial<AuthService>;
|
||||||
|
|
||||||
@Post('login')
|
beforeEach(async () => {
|
||||||
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
|
mockAuthService = {
|
||||||
async login(@Body() loginDto: LoginDto) {
|
validateUser: jest.fn(),
|
||||||
const user = await this.authService.validateUser(
|
login: jest.fn(),
|
||||||
loginDto.username,
|
register: jest.fn(),
|
||||||
loginDto.password,
|
refreshToken: jest.fn(),
|
||||||
);
|
logout: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
if (!user) {
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
controllers: [AuthController],
|
||||||
}
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: mockAuthService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
return this.authService.login(user);
|
controller = module.get<AuthController>(AuthController);
|
||||||
}
|
});
|
||||||
|
|
||||||
@Post('register-admin')
|
it('should be defined', () => {
|
||||||
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
|
expect(controller).toBeDefined();
|
||||||
async register(@Body() registerDto: RegisterDto) {
|
});
|
||||||
return this.authService.register(registerDto);
|
|
||||||
}
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,17 +11,25 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Throttle } from '@nestjs/throttler';
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { AuthService } from './auth.service.js';
|
import { AuthService } from './auth.service';
|
||||||
import { LoginDto } from './dto/login.dto.js';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { RegisterDto } from './dto/register.dto.js';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard.js';
|
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import {
|
||||||
import { Request } from 'express'; // ✅ Import Request
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBody,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
// สร้าง Interface สำหรับ Request ที่มี User (เพื่อให้ TS รู้จัก req.user)
|
// สร้าง Interface สำหรับ Request ที่มี User
|
||||||
interface RequestWithUser extends Request {
|
interface RequestWithUser extends Request {
|
||||||
user: any;
|
user: any;
|
||||||
}
|
}
|
||||||
@@ -34,11 +42,24 @@ export class AuthController {
|
|||||||
@Post('login')
|
@Post('login')
|
||||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'เข้าสู่ระบบเพื่อรับ Access & Refresh Token' })
|
@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' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
async login(@Body() loginDto: LoginDto) {
|
async login(@Body() loginDto: LoginDto) {
|
||||||
const user = await this.authService.validateUser(
|
const user = await this.authService.validateUser(
|
||||||
loginDto.username,
|
loginDto.username,
|
||||||
loginDto.password,
|
loginDto.password
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -51,7 +72,9 @@ export class AuthController {
|
|||||||
@Post('register-admin')
|
@Post('register-admin')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'สร้างบัญชีผู้ใช้ใหม่ (Admin Only)' })
|
@ApiOperation({ summary: 'Create new user (Admin Only)' })
|
||||||
|
@ApiBody({ type: RegisterDto })
|
||||||
|
@ApiResponse({ status: 201, description: 'User registered' })
|
||||||
async register(@Body() registerDto: RegisterDto) {
|
async register(@Body() registerDto: RegisterDto) {
|
||||||
return this.authService.register(registerDto);
|
return this.authService.register(registerDto);
|
||||||
}
|
}
|
||||||
@@ -59,9 +82,20 @@ export class AuthController {
|
|||||||
@UseGuards(JwtRefreshGuard)
|
@UseGuards(JwtRefreshGuard)
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'ขอ Access Token ใหม่ด้วย Refresh Token' })
|
@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' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
async refresh(@Req() req: RequestWithUser) {
|
async refresh(@Req() req: RequestWithUser) {
|
||||||
// ✅ ระบุ Type ชัดเจน
|
|
||||||
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,23 +103,51 @@ export class AuthController {
|
|||||||
@Post('logout')
|
@Post('logout')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'ออกจากระบบ (Revoke Token)' })
|
@ApiOperation({ summary: 'Logout (Revoke Tokens)' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Logged out successfully',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string', example: 'Logged out successfully' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
async logout(@Req() req: RequestWithUser) {
|
async logout(@Req() req: RequestWithUser) {
|
||||||
// ✅ ระบุ Type ชัดเจน
|
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
// ต้องเช็คว่ามี token หรือไม่ เพื่อป้องกัน runtime error
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return { message: 'No token provided' };
|
return { message: 'No token provided' };
|
||||||
}
|
}
|
||||||
|
// ส่ง refresh token ไปด้วยถ้ามี (ใน header หรือ body)
|
||||||
|
// สำหรับตอนนี้ส่งแค่ access token ไป blacklist
|
||||||
return this.authService.logout(req.user.sub, token);
|
return this.authService.logout(req.user.sub, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('profile')
|
@Get('profile')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' })
|
@ApiOperation({ summary: 'Get current user profile' })
|
||||||
|
@ApiResponse({ status: 200, description: 'User profile' })
|
||||||
getProfile(@Req() req: RequestWithUser) {
|
getProfile(@Req() req: RequestWithUser) {
|
||||||
// ✅ ระบุ Type ชัดเจน
|
|
||||||
return req.user;
|
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,6 +1,7 @@
|
|||||||
// File: src/common/auth/auth.module.ts
|
// File: src/common/auth/auth.module.ts
|
||||||
// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322)
|
// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322)
|
||||||
// [P0-1] เพิ่ม CASL RBAC Integration
|
// [P0-1] เพิ่ม CASL RBAC Integration
|
||||||
|
// [P2-2] Register RefreshToken Entity
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@@ -13,18 +14,19 @@ import { UserModule } from '../../modules/user/user.module.js';
|
|||||||
import { JwtStrategy } from './strategies/jwt.strategy.js';
|
import { JwtStrategy } from './strategies/jwt.strategy.js';
|
||||||
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js';
|
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js';
|
||||||
import { User } from '../../modules/user/entities/user.entity';
|
import { User } from '../../modules/user/entities/user.entity';
|
||||||
import { CaslModule } from './casl/casl.module'; // [P0-1] Import CASL
|
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'; // [P0-1] Import Guard
|
import { CaslModule } from './casl/casl.module';
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([User]),
|
TypeOrmModule.forFeature([User, RefreshToken]), // [P2-2] Added RefreshToken
|
||||||
UserModule,
|
UserModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('JWT_SECRET'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
expiresIn: (configService.get<string>('JWT_EXPIRATION') ||
|
expiresIn: (configService.get<string>('JWT_EXPIRATION') ||
|
||||||
@@ -32,18 +34,10 @@ import { PermissionsGuard } from './guards/permissions.guard'; // [P0-1] Import
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
CaslModule, // [P0-1] Import CASL module
|
CaslModule,
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
AuthService,
|
|
||||||
JwtStrategy,
|
|
||||||
JwtRefreshStrategy,
|
|
||||||
PermissionsGuard, // [P0-1] Register PermissionsGuard
|
|
||||||
],
|
],
|
||||||
|
providers: [AuthService, JwtStrategy, JwtRefreshStrategy, PermissionsGuard],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [
|
exports: [AuthService, PermissionsGuard],
|
||||||
AuthService,
|
|
||||||
PermissionsGuard, // [P0-1] Export for use in other modules
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,18 +1,201 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AuthService } from './auth.service';
|
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', () => {
|
describe('AuthService', () => {
|
||||||
let service: 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 () => {
|
beforeEach(async () => {
|
||||||
|
// Reset bcrypt mocks
|
||||||
|
bcrypt.compare.mockResolvedValue(true);
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [AuthService],
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<AuthService>(AuthService);
|
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', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
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,6 +2,7 @@
|
|||||||
// บันทึกการแก้ไข:
|
// บันทึกการแก้ไข:
|
||||||
// 1. แก้ไข Type Mismatch ใน signAsync
|
// 1. แก้ไข Type Mismatch ใน signAsync
|
||||||
// 2. แก้ไข validateUser ให้ดึง password_hash ออกมาด้วย (Fix HTTP 500: data and hash arguments required)
|
// 2. แก้ไข validateUser ให้ดึง password_hash ออกมาด้วย (Fix HTTP 500: data and hash arguments required)
|
||||||
|
// 3. [P2-2] Implement Refresh Token storage & rotation
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -12,14 +13,16 @@ import {
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { InjectRepository } from '@nestjs/typeorm'; // [NEW]
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm'; // [NEW]
|
import { Repository } from 'typeorm';
|
||||||
import type { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
import { UserService } from '../../modules/user/user.service.js';
|
import { UserService } from '../../modules/user/user.service';
|
||||||
import { User } from '../../modules/user/entities/user.entity.js'; // [NEW] ต้อง Import Entity เพื่อใช้ Repository
|
import { User } from '../../modules/user/entities/user.entity';
|
||||||
import { RegisterDto } from './dto/register.dto.js';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -28,31 +31,27 @@ export class AuthService {
|
|||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
// [NEW] Inject Repository เพื่อใช้ QueryBuilder
|
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private usersRepository: Repository<User>,
|
private usersRepository: Repository<User>,
|
||||||
|
// [P2-2] Inject RefreshToken Repository
|
||||||
|
@InjectRepository(RefreshToken)
|
||||||
|
private refreshTokenRepository: Repository<RefreshToken>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// 1. ตรวจสอบ Username/Password
|
// 1. ตรวจสอบ Username/Password
|
||||||
async validateUser(username: string, pass: string): Promise<any> {
|
async validateUser(username: string, pass: string): Promise<any> {
|
||||||
console.log(`🔍 Checking login for: ${username}`); // [DEBUG]
|
console.log(`🔍 Checking login for: ${username}`);
|
||||||
// [FIXED] ใช้ createQueryBuilder เพื่อ addSelect field 'password' ที่ถูกซ่อนไว้
|
|
||||||
const user = await this.usersRepository
|
const user = await this.usersRepository
|
||||||
.createQueryBuilder('user')
|
.createQueryBuilder('user')
|
||||||
.addSelect('user.password') // สำคัญ! สั่งให้ดึง column password มาด้วย
|
.addSelect('user.password')
|
||||||
.where('user.username = :username', { username })
|
.where('user.username = :username', { username })
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log('❌ User not found in database'); // [DEBUG]
|
console.log('❌ User not found in database');
|
||||||
return null;
|
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 หรือไม่
|
// ตรวจสอบว่ามี user และมี password hash หรือไม่
|
||||||
if (user && user.password && (await bcrypt.compare(pass, user.password))) {
|
if (user && user.password && (await bcrypt.compare(pass, user.password))) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -62,7 +61,7 @@ export class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Login: สร้าง Access & Refresh Token
|
// 2. Login: สร้าง Access & Refresh Token และบันทึกลง DB
|
||||||
async login(user: any) {
|
async login(user: any) {
|
||||||
const payload = {
|
const payload = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -70,20 +69,20 @@ export class AuthService {
|
|||||||
scope: 'Global',
|
scope: 'Global',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [accessToken, refreshToken] = await Promise.all([
|
const accessToken = await this.jwtService.signAsync(payload, {
|
||||||
this.jwtService.signAsync(payload, {
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||||
// ✅ Fix: Cast as any
|
'15m') as any,
|
||||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
});
|
||||||
'15m') as any,
|
|
||||||
}),
|
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||||
this.jwtService.signAsync(payload, {
|
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||||
// ✅ Fix: Cast as any
|
'7d') as any,
|
||||||
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);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
@@ -92,10 +91,28 @@ 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)
|
// 3. Register (สำหรับ Admin)
|
||||||
async register(userDto: RegisterDto) {
|
async register(userDto: RegisterDto) {
|
||||||
const existingUser = await this.userService.findOneByUsername(
|
const existingUser = await this.userService.findOneByUsername(
|
||||||
userDto.username,
|
userDto.username
|
||||||
);
|
);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new BadRequestException('Username already exists');
|
throw new BadRequestException('Username already exists');
|
||||||
@@ -110,27 +127,79 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Refresh Token: ออก Token ใหม่
|
// 4. Refresh Token: ตรวจสอบและออก Token ใหม่ (Rotation)
|
||||||
async refreshToken(userId: number, refreshToken: string) {
|
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);
|
const user = await this.userService.findOne(userId);
|
||||||
if (!user) throw new UnauthorizedException('User not found');
|
if (!user) throw new UnauthorizedException('User not found');
|
||||||
|
|
||||||
const payload = { username: user.username, sub: user.user_id };
|
const payload = { username: user.username, sub: user.user_id };
|
||||||
|
|
||||||
const accessToken = await this.jwtService.signAsync(payload, {
|
// Generate NEW tokens
|
||||||
|
const newAccessToken = await this.jwtService.signAsync(payload, {
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
// ✅ Fix: Cast as any
|
|
||||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||||
'15m') as any,
|
'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 {
|
return {
|
||||||
access_token: accessToken,
|
access_token: newAccessToken,
|
||||||
|
refresh_token: newRefreshToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Logout: นำ Token เข้า Blacklist ใน Redis
|
// [P2-2] Helper: Revoke all tokens for a user (Security Measure)
|
||||||
async logout(userId: number, accessToken: string) {
|
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
|
||||||
try {
|
try {
|
||||||
const decoded = this.jwtService.decode(accessToken);
|
const decoded = this.jwtService.decode(accessToken);
|
||||||
if (decoded && decoded.exp) {
|
if (decoded && decoded.exp) {
|
||||||
@@ -139,13 +208,65 @@ export class AuthService {
|
|||||||
await this.cacheManager.set(
|
await this.cacheManager.set(
|
||||||
`blacklist:token:${accessToken}`,
|
`blacklist:token:${accessToken}`,
|
||||||
true,
|
true,
|
||||||
ttl * 1000,
|
ttl * 1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore decoding 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' };
|
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,11 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
|
||||||
Ability,
|
|
||||||
AbilityBuilder,
|
|
||||||
AbilityClass,
|
|
||||||
ExtractSubjectType,
|
|
||||||
InferSubjects,
|
|
||||||
} from '@casl/ability';
|
|
||||||
import { User } from '../../../modules/user/entities/user.entity';
|
import { User } from '../../../modules/user/entities/user.entity';
|
||||||
import { UserAssignment } from '../../../modules/user/entities/user-assignment.entity';
|
import { UserAssignment } from '../../../modules/user/entities/user-assignment.entity';
|
||||||
|
|
||||||
@@ -45,7 +39,7 @@ export class AbilityFactory {
|
|||||||
* - Level 4: Contract
|
* - Level 4: Contract
|
||||||
*/
|
*/
|
||||||
createForUser(user: User, context: ScopeContext): AppAbility {
|
createForUser(user: User, context: ScopeContext): AppAbility {
|
||||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
|
const { can, build } = new AbilityBuilder<AppAbility>(
|
||||||
Ability as AbilityClass<AppAbility>
|
Ability as AbilityClass<AppAbility>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,12 +48,13 @@ export class AbilityFactory {
|
|||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iterate through user's role assignments
|
||||||
// Iterate through user's role assignments
|
// Iterate through user's role assignments
|
||||||
user.assignments.forEach((assignment: UserAssignment) => {
|
user.assignments.forEach((assignment: UserAssignment) => {
|
||||||
// Check if assignment matches the current context
|
// Check if assignment matches the current context
|
||||||
if (this.matchesScope(assignment, context)) {
|
if (this.matchesScope(assignment, context)) {
|
||||||
// Grant permissions from the role
|
// Grant permissions from the role
|
||||||
assignment.role.permissions.forEach((permission) => {
|
assignment.role.permissions?.forEach((permission) => {
|
||||||
const [action, subject] = this.parsePermission(
|
const [action, subject] = this.parsePermission(
|
||||||
permission.permissionName
|
permission.permissionName
|
||||||
);
|
);
|
||||||
@@ -70,8 +65,10 @@ export class AbilityFactory {
|
|||||||
|
|
||||||
return build({
|
return build({
|
||||||
// Detect subject type (for future use with objects)
|
// Detect subject type (for future use with objects)
|
||||||
detectSubjectType: (item) =>
|
detectSubjectType: (item: any) => {
|
||||||
item.constructor as ExtractSubjectType<Subjects>,
|
if (typeof item === 'string') return item;
|
||||||
|
return item.constructor;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,17 +117,17 @@ export class AbilityFactory {
|
|||||||
* "project.view" → ["view", "project"]
|
* "project.view" → ["view", "project"]
|
||||||
*/
|
*/
|
||||||
private parsePermission(permissionName: string): [string, string] {
|
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('.');
|
const parts = permissionName.split('.');
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
const [subject, action] = parts;
|
const [subject, action] = parts;
|
||||||
return [action, subject];
|
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}`);
|
throw new Error(`Invalid permission format: ${permissionName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Username (Email)',
|
||||||
|
example: 'admin@np-dms.work',
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
username!: string;
|
username!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Password', example: 'password123' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|||||||
38
backend/src/common/auth/entities/refresh-token.entity.ts
Normal file
38
backend/src/common/auth/entities/refresh-token.entity.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
// Check if user has ALL required permissions
|
||||||
const hasPermission = requiredPermissions.every((permission) => {
|
const hasPermission = requiredPermissions.every((permission) => {
|
||||||
const [action, subject] = this.parsePermission(permission);
|
const [action, subject] = this.parsePermission(permission);
|
||||||
return ability.can(action, subject);
|
return ability.can(action as any, subject as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ export class AuditLog {
|
|||||||
@Column({ name: 'user_agent', length: 255, nullable: true })
|
@Column({ name: 'user_agent', length: 255, nullable: true })
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
|
|
||||||
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
|
// ✅ [Fix] ทั้งสอง Decorator ต้องระบุ name: 'created_at'
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
|
@PrimaryColumn({ name: 'created_at' }) // Composite PK คู่กับ auditId
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import {
|
import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
DeleteDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
export abstract class BaseEntity {
|
export abstract class BaseEntity {
|
||||||
// @PrimaryGeneratedColumn()
|
// @PrimaryGeneratedColumn()
|
||||||
// id!: number;
|
// id!: number;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
created_at!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at' })
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
updated_at!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
|
@DeleteDateColumn({ name: 'deleted_at', select: false })
|
||||||
deleted_at!: Date;
|
deletedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../../modules/user/entities/user.entity.js';
|
import { User } from '../../../modules/user/entities/user.entity';
|
||||||
|
|
||||||
@Entity('attachments')
|
@Entity('attachments')
|
||||||
export class Attachment {
|
export class Attachment {
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { FileStorageController } from './file-storage.controller';
|
import { FileStorageController } from './file-storage.controller';
|
||||||
|
import { FileStorageService } from './file-storage.service';
|
||||||
|
|
||||||
describe('FileStorageController', () => {
|
describe('FileStorageController', () => {
|
||||||
let controller: FileStorageController;
|
let controller: FileStorageController;
|
||||||
|
let mockFileStorageService: Partial<FileStorageService>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
mockFileStorageService = {
|
||||||
|
upload: jest.fn(),
|
||||||
|
download: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [FileStorageController],
|
controllers: [FileStorageController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: FileStorageService,
|
||||||
|
useValue: mockFileStorageService,
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<FileStorageController>(FileStorageController);
|
controller = module.get<FileStorageController>(FileStorageController);
|
||||||
@@ -15,4 +29,25 @@ describe('FileStorageController', () => {
|
|||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
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';
|
} from '@nestjs/common';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { FileStorageService } from './file-storage.service.js';
|
import { FileStorageService } from './file-storage.service';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
|
|
||||||
// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว
|
// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
@@ -47,10 +47,10 @@ export class FileStorageController {
|
|||||||
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
|
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser
|
||||||
) {
|
) {
|
||||||
// ส่ง userId จาก Token ไปด้วย
|
// ส่ง userId จาก Token ไปด้วย
|
||||||
return this.fileStorageService.upload(file, req.user.userId);
|
return this.fileStorageService.upload(file, req.user.userId);
|
||||||
@@ -63,7 +63,7 @@ export class FileStorageController {
|
|||||||
@Get(':id/download')
|
@Get(':id/download')
|
||||||
async downloadFile(
|
async downloadFile(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response
|
||||||
): Promise<StreamableFile> {
|
): Promise<StreamableFile> {
|
||||||
const { stream, attachment } = await this.fileStorageService.download(id);
|
const { stream, attachment } = await this.fileStorageService.download(id);
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ export class FileStorageController {
|
|||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async deleteFile(
|
async deleteFile(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Request() req: RequestWithUser,
|
@Request() req: RequestWithUser
|
||||||
) {
|
) {
|
||||||
// ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ
|
// ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ
|
||||||
await this.fileStorageService.delete(id, req.user.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 { FileStorageService } from './file-storage.service.js';
|
||||||
import { FileStorageController } from './file-storage.controller.js';
|
import { FileStorageController } from './file-storage.controller.js';
|
||||||
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
||||||
import { Attachment } from './entities/attachment.entity.js';
|
import { Attachment } from './entities/attachment.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -1,18 +1,142 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { FileStorageService } from './file-storage.service';
|
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', () => {
|
describe('FileStorageService', () => {
|
||||||
let service: 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 () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [FileStorageService],
|
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;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<FileStorageService>(FileStorageService);
|
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', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
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 path from 'path';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Attachment } from './entities/attachment.entity.js';
|
import { Attachment } from './entities/attachment.entity';
|
||||||
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ import {
|
|||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js';
|
import { PERMISSIONS_KEY } from '../decorators/require-permission.decorator';
|
||||||
import { UserService } from '../../modules/user/user.service.js';
|
import { UserService } from '../../modules/user/user.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RbacGuard implements CanActivate {
|
export class RbacGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private userService: UserService,
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
|
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
|
||||||
const requiredPermission = this.reflector.getAllAndOverride<string>(
|
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||||
PERMISSION_KEY,
|
PERMISSIONS_KEY,
|
||||||
[context.getHandler(), context.getClass()],
|
[context.getHandler(), context.getClass()]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
|
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
|
||||||
if (!requiredPermission) {
|
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,19 +34,20 @@ export class RbacGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
|
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
|
||||||
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
|
|
||||||
const userPermissions = await this.userService.getUserPermissions(
|
const userPermissions = await this.userService.getUserPermissions(
|
||||||
user.userId,
|
user.user_id // ✅ FIX: ใช้ user_id ตาม Entity field name
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม?
|
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? (User ต้องมีครบทุกสิทธิ์)
|
||||||
const hasPermission = userPermissions.some(
|
const hasPermission = requiredPermissions.every((req) =>
|
||||||
(p) => p === requiredPermission || p === 'system.manage_all', // Superadmin ทะลุทุกสิทธิ์
|
userPermissions.some(
|
||||||
|
(p) => p === req || p === 'system.manage_all' // Superadmin ทะลุทุกสิทธิ์
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
`You do not have permission: ${requiredPermission}`,
|
`You do not have permission: ${requiredPermissions.join(', ')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface Response<T> {
|
|||||||
statusCode: number;
|
statusCode: number;
|
||||||
message: string;
|
message: string;
|
||||||
data: T;
|
data: T;
|
||||||
|
meta?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -19,14 +20,29 @@ export class TransformInterceptor<T>
|
|||||||
{
|
{
|
||||||
intercept(
|
intercept(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
next: CallHandler,
|
next: CallHandler
|
||||||
): Observable<Response<T>> {
|
): Observable<Response<T>> {
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
map((data) => ({
|
map((data: any) => {
|
||||||
statusCode: context.switchToHttp().getResponse().statusCode,
|
const response = context.switchToHttp().getResponse();
|
||||||
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
|
|
||||||
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
|
// 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,
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { Organization } from '../../modules/organizations/entities/organization.entity';
|
import { Organization } from '../../modules/organization/entities/organization.entity';
|
||||||
|
|
||||||
export async function seedOrganizations(dataSource: DataSource) {
|
export async function seedOrganizations(dataSource: DataSource) {
|
||||||
const repo = dataSource.getRepository(Organization);
|
const repo = dataSource.getRepository(Organization);
|
||||||
|
|||||||
@@ -1,45 +1,54 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { User } from '../../modules/users/entities/user.entity';
|
import { User } from '../../modules/user/entities/user.entity';
|
||||||
import { Role } from '../../modules/auth/entities/role.entity';
|
import { Role, RoleScope } from '../../modules/user/entities/role.entity';
|
||||||
|
import { UserAssignment } from '../../modules/user/entities/user-assignment.entity';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
export async function seedUsers(dataSource: DataSource) {
|
export async function seedUsers(dataSource: DataSource) {
|
||||||
const userRepo = dataSource.getRepository(User);
|
const userRepo = dataSource.getRepository(User);
|
||||||
const roleRepo = dataSource.getRepository(Role);
|
const roleRepo = dataSource.getRepository(Role);
|
||||||
|
const assignmentRepo = dataSource.getRepository(UserAssignment);
|
||||||
|
|
||||||
// Create Roles
|
// Create Roles
|
||||||
const rolesData = [
|
const rolesData = [
|
||||||
{
|
{
|
||||||
roleName: 'Superadmin',
|
roleName: 'Superadmin',
|
||||||
|
scope: RoleScope.GLOBAL,
|
||||||
description:
|
description:
|
||||||
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',
|
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Org Admin',
|
roleName: 'Org Admin',
|
||||||
|
scope: RoleScope.ORGANIZATION,
|
||||||
description:
|
description:
|
||||||
'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',
|
'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Document Control',
|
roleName: 'Document Control',
|
||||||
|
scope: RoleScope.ORGANIZATION,
|
||||||
description:
|
description:
|
||||||
'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',
|
'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Editor',
|
roleName: 'Editor',
|
||||||
|
scope: RoleScope.PROJECT,
|
||||||
description:
|
description:
|
||||||
'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',
|
'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Viewer',
|
roleName: 'Viewer',
|
||||||
|
scope: RoleScope.PROJECT,
|
||||||
description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',
|
description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Project Manager',
|
roleName: 'Project Manager',
|
||||||
|
scope: RoleScope.PROJECT,
|
||||||
description:
|
description:
|
||||||
'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',
|
'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleName: 'Contract Admin',
|
roleName: 'Contract Admin',
|
||||||
|
scope: RoleScope.CONTRACT,
|
||||||
description:
|
description:
|
||||||
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',
|
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',
|
||||||
},
|
},
|
||||||
@@ -49,6 +58,7 @@ export async function seedUsers(dataSource: DataSource) {
|
|||||||
for (const r of rolesData) {
|
for (const r of rolesData) {
|
||||||
let role = await roleRepo.findOneBy({ roleName: r.roleName });
|
let role = await roleRepo.findOneBy({ roleName: r.roleName });
|
||||||
if (!role) {
|
if (!role) {
|
||||||
|
// @ts-ignore
|
||||||
role = await roleRepo.save(roleRepo.create(r));
|
role = await roleRepo.save(roleRepo.create(r));
|
||||||
}
|
}
|
||||||
roleMap.set(r.roleName, role);
|
roleMap.set(r.roleName, role);
|
||||||
@@ -87,20 +97,30 @@ export async function seedUsers(dataSource: DataSource) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
const passwordHash = await bcrypt.hash('password123', salt); // Default password
|
const password = await bcrypt.hash('password123', salt); // Default password
|
||||||
|
|
||||||
for (const u of usersData) {
|
for (const u of usersData) {
|
||||||
const exists = await userRepo.findOneBy({ username: u.username });
|
let user = await userRepo.findOneBy({ username: u.username });
|
||||||
if (!exists) {
|
if (!user) {
|
||||||
const user = userRepo.create({
|
user = userRepo.create({
|
||||||
username: u.username,
|
username: u.username,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
firstName: u.firstName,
|
firstName: u.firstName,
|
||||||
lastName: u.lastName,
|
lastName: u.lastName,
|
||||||
passwordHash,
|
password, // Fixed: password instead of passwordHash
|
||||||
roles: [roleMap.get(u.roleName)],
|
|
||||||
});
|
});
|
||||||
await userRepo.save(user);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,24 @@ async function bootstrap() {
|
|||||||
const logger = new Logger('Bootstrap');
|
const logger = new Logger('Bootstrap');
|
||||||
|
|
||||||
// 🛡️ 2. Security (Helmet & CORS)
|
// 🛡️ 2. Security (Helmet & CORS)
|
||||||
app.use(helmet());
|
// ปรับ 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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// ตั้งค่า CORS (ใน Production ควรระบุ origin ให้ชัดเจนจาก Config)
|
// ตั้งค่า CORS (ใน Production ควรระบุ origin ให้ชัดเจนจาก Config)
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: true, // หรือ configService.get('CORS_ORIGIN')
|
origin: configService.get<string>('CORS_ORIGIN') || true,
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
});
|
||||||
@@ -47,7 +60,7 @@ async function bootstrap() {
|
|||||||
transformOptions: {
|
transformOptions: {
|
||||||
enableImplicitConversion: true, // ช่วยแปลง Type ใน Query Params
|
enableImplicitConversion: true, // ช่วยแปลง Type ใน Query Params
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// ลงทะเบียน Global Interceptor และ Filter ที่เราสร้างไว้
|
// ลงทะเบียน Global Interceptor และ Filter ที่เราสร้างไว้
|
||||||
@@ -73,9 +86,9 @@ async function bootstrap() {
|
|||||||
|
|
||||||
// 🚀 7. Start Server
|
// 🚀 7. Start Server
|
||||||
const port = configService.get<number>('PORT') || 3001;
|
const port = configService.get<number>('PORT') || 3001;
|
||||||
await app.listen(port);
|
await app.listen(port, '0.0.0.0');
|
||||||
|
|
||||||
logger.log(`Application is running on: ${await app.getUrl()}/api`);
|
logger.log(`Application is running on: ${await app.getUrl()}/api`);
|
||||||
logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`);
|
logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`);
|
||||||
}
|
}
|
||||||
bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
17
backend/src/modules/audit-log/audit-log.controller.ts
Normal file
17
backend/src/modules/audit-log/audit-log.controller.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/src/modules/audit-log/audit-log.module.ts
Normal file
14
backend/src/modules/audit-log/audit-log.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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 {}
|
||||||
48
backend/src/modules/audit-log/audit-log.service.ts
Normal file
48
backend/src/modules/audit-log/audit-log.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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,36 +5,55 @@ import {
|
|||||||
ManyToMany,
|
ManyToMany,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
|
|
||||||
@Entity('permissions')
|
@Entity('permissions')
|
||||||
export class Permission {
|
export class Permission extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn({ name: 'permission_id' })
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'permission_code', length: 50, unique: true })
|
@Column({ name: 'permission_name', length: 100, unique: true })
|
||||||
permissionCode!: string;
|
permissionName!: string;
|
||||||
|
|
||||||
@Column({ name: 'description', type: 'text', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
@Column({ name: 'resource', length: 50 })
|
@Column({ name: 'module', length: 50, nullable: true })
|
||||||
resource!: string;
|
module?: string;
|
||||||
|
|
||||||
@Column({ name: 'action', length: 50 })
|
@Column({
|
||||||
action!: string;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity('roles')
|
@Entity('roles')
|
||||||
export class Role {
|
export class Role extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn({ name: 'role_id' })
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'role_name', length: 50, unique: true })
|
@Column({ name: 'role_name', length: 100, unique: true })
|
||||||
roleName!: string;
|
roleName!: string;
|
||||||
|
|
||||||
@Column({ name: 'description', type: 'text', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
description!: string;
|
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)
|
@ManyToMany(() => Permission)
|
||||||
@JoinTable({
|
@JoinTable({
|
||||||
name: 'role_permissions',
|
name: 'role_permissions',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { User } from '../user/entities/user.entity';
|
|||||||
import { CreateCirculationDto } from './dto/create-circulation.dto';
|
import { CreateCirculationDto } from './dto/create-circulation.dto';
|
||||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
|
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
|
||||||
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
||||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CirculationService {
|
export class CirculationService {
|
||||||
@@ -37,9 +37,9 @@ export class CirculationService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||||
const circulationNo = await this.numberingService.generateNextNumber({
|
const result = await this.numberingService.generateNextNumber({
|
||||||
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
||||||
originatorId: user.primaryOrganizationId,
|
originatorOrganizationId: user.primaryOrganizationId,
|
||||||
typeId: 900, // Fixed Type ID for Circulation
|
typeId: 900, // Fixed Type ID for Circulation
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
customTokens: {
|
customTokens: {
|
||||||
@@ -51,7 +51,7 @@ export class CirculationService {
|
|||||||
const circulation = queryRunner.manager.create(Circulation, {
|
const circulation = queryRunner.manager.create(Circulation, {
|
||||||
organizationId: user.primaryOrganizationId,
|
organizationId: user.primaryOrganizationId,
|
||||||
correspondenceId: createDto.correspondenceId,
|
correspondenceId: createDto.correspondenceId,
|
||||||
circulationNo: circulationNo,
|
circulationNo: result.number,
|
||||||
subject: createDto.subject,
|
subject: createDto.subject,
|
||||||
statusCode: 'OPEN',
|
statusCode: 'OPEN',
|
||||||
createdByUserId: user.user_id,
|
createdByUserId: user.user_id,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Circulation } from './circulation.entity';
|
import { Circulation } from './circulation.entity';
|
||||||
import { Organization } from '../../project/entities/organization.entity';
|
import { Organization } from '../../organization/entities/organization.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
|
||||||
@Entity('circulation_routings')
|
@Entity('circulation_routings')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
OneToMany,
|
OneToMany,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||||
import { Organization } from '../../project/entities/organization.entity';
|
import { Organization } from '../../organization/entities/organization.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||||
import { CirculationRouting } from './circulation-routing.entity';
|
import { CirculationRouting } from './circulation-routing.entity';
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { ContractService } from './contract.service.js';
|
import { ContractService } from './contract.service.js';
|
||||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||||
|
import { SearchContractDto } from './dto/search-contract.dto.js';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||||
|
|
||||||
@@ -38,11 +39,10 @@ export class ContractController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get All Contracts (Optional: filter by projectId)',
|
summary: 'Get All Contracts (Search & Filter)',
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
findAll(@Query() query: SearchContractDto) {
|
||||||
findAll(@Query('projectId') projectId?: number) {
|
return this.contractService.findAll(query);
|
||||||
return this.contractService.findAll(projectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
18
backend/src/modules/contract/contract.module.ts
Normal file
18
backend/src/modules/contract/contract.module.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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 {}
|
||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
ConflictException,
|
ConflictException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository, Like } from 'typeorm';
|
||||||
import { Contract } from './entities/contract.entity.js';
|
import { Contract } from './entities/contract.entity';
|
||||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||||
|
|
||||||
@@ -29,17 +29,53 @@ export class ContractService {
|
|||||||
return this.contractRepo.save(contract);
|
return this.contractRepo.save(contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(projectId?: number) {
|
async findAll(params?: any) {
|
||||||
const query = this.contractRepo
|
const { search, projectId, page = 1, limit = 100 } = params || {};
|
||||||
.createQueryBuilder('c')
|
const skip = (page - 1) * limit;
|
||||||
.leftJoinAndSelect('c.project', 'p')
|
|
||||||
.orderBy('c.contractCode', 'ASC');
|
|
||||||
|
|
||||||
if (projectId) {
|
const findOptions: any = {
|
||||||
query.where('c.projectId = :projectId', { projectId });
|
relations: ['project'],
|
||||||
|
order: { contractCode: 'ASC' },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
where: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchConditions = [];
|
||||||
|
if (search) {
|
||||||
|
searchConditions.push({ contractCode: Like(`%${search}%`) });
|
||||||
|
searchConditions.push({ contractName: Like(`%${search}%`) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.getMany();
|
if (projectId) {
|
||||||
|
// Combine project filter with search if exists
|
||||||
|
if (searchConditions.length > 0) {
|
||||||
|
findOptions.where = searchConditions.map((cond) => ({
|
||||||
|
...cond,
|
||||||
|
projectId,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
findOptions.where = { projectId };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (searchConditions.length > 0) {
|
||||||
|
findOptions.where = searchConditions;
|
||||||
|
} else {
|
||||||
|
delete findOptions.where; // No filters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.contractRepo.findAndCount(findOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number) {
|
||||||
30
backend/src/modules/contract/dto/search-contract.dto.ts
Normal file
30
backend/src/modules/contract/dto/search-contract.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
import { Contract } from './contract.entity.js';
|
import { Contract } from './contract.entity';
|
||||||
import { Organization } from './organization.entity.js';
|
import { Organization } from '../../organization/entities/organization.entity';
|
||||||
|
|
||||||
@Entity('contract_organizations')
|
@Entity('contract_organizations')
|
||||||
export class ContractOrganization {
|
export class ContractOrganization {
|
||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity.js';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Project } from './project.entity.js';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
|
|
||||||
@Entity('contracts')
|
@Entity('contracts')
|
||||||
export class Contract extends BaseEntity {
|
export class Contract extends BaseEntity {
|
||||||
@@ -23,13 +23,14 @@ export class CorrespondenceWorkflowService {
|
|||||||
private readonly revisionRepo: Repository<CorrespondenceRevision>,
|
private readonly revisionRepo: Repository<CorrespondenceRevision>,
|
||||||
@InjectRepository(CorrespondenceStatus)
|
@InjectRepository(CorrespondenceStatus)
|
||||||
private readonly statusRepo: Repository<CorrespondenceStatus>,
|
private readonly statusRepo: Repository<CorrespondenceStatus>,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async submitWorkflow(
|
async submitWorkflow(
|
||||||
correspondenceId: number,
|
correspondenceId: number,
|
||||||
userId: number,
|
userId: number,
|
||||||
note?: string,
|
userRoles: string[], // [FIX] Added roles for DSL requirements check
|
||||||
|
note?: string
|
||||||
) {
|
) {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
@@ -44,7 +45,7 @@ export class CorrespondenceWorkflowService {
|
|||||||
|
|
||||||
if (!revision) {
|
if (!revision) {
|
||||||
throw new NotFoundException(
|
throw new NotFoundException(
|
||||||
`Correspondence Revision for ID ${correspondenceId} not found`,
|
`Correspondence Revision for ID ${correspondenceId} not found`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export class CorrespondenceWorkflowService {
|
|||||||
this.WORKFLOW_CODE,
|
this.WORKFLOW_CODE,
|
||||||
'correspondence_revision',
|
'correspondence_revision',
|
||||||
revision.id.toString(),
|
revision.id.toString(),
|
||||||
context,
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
const transitionResult = await this.workflowEngine.processTransition(
|
const transitionResult = await this.workflowEngine.processTransition(
|
||||||
@@ -74,7 +75,7 @@ export class CorrespondenceWorkflowService {
|
|||||||
'SUBMIT',
|
'SUBMIT',
|
||||||
userId,
|
userId,
|
||||||
note || 'Initial Submission',
|
note || 'Initial Submission',
|
||||||
{},
|
{ roles: userRoles } // [FIX] Pass roles for DSL requirements check
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.syncStatus(revision, transitionResult.nextState, queryRunner);
|
await this.syncStatus(revision, transitionResult.nextState, queryRunner);
|
||||||
@@ -97,14 +98,14 @@ export class CorrespondenceWorkflowService {
|
|||||||
async processAction(
|
async processAction(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
dto: WorkflowTransitionDto,
|
dto: WorkflowTransitionDto
|
||||||
) {
|
) {
|
||||||
const result = await this.workflowEngine.processTransition(
|
const result = await this.workflowEngine.processTransition(
|
||||||
instanceId,
|
instanceId,
|
||||||
dto.action,
|
dto.action,
|
||||||
userId,
|
userId,
|
||||||
dto.comment,
|
dto.comment,
|
||||||
dto.payload,
|
dto.payload
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ FIX: Method exists now
|
// ✅ FIX: Method exists now
|
||||||
@@ -125,7 +126,7 @@ export class CorrespondenceWorkflowService {
|
|||||||
private async syncStatus(
|
private async syncStatus(
|
||||||
revision: CorrespondenceRevision,
|
revision: CorrespondenceRevision,
|
||||||
workflowState: string,
|
workflowState: string,
|
||||||
queryRunner?: any,
|
queryRunner?: any
|
||||||
) {
|
) {
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
DRAFT: 'DRAFT',
|
DRAFT: 'DRAFT',
|
||||||
|
|||||||
@@ -1,13 +1,48 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { CorrespondenceController } from './correspondence.controller';
|
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', () => {
|
describe('CorrespondenceController', () => {
|
||||||
let controller: CorrespondenceController;
|
let controller: CorrespondenceController;
|
||||||
|
let mockCorrespondenceService: Partial<CorrespondenceService>;
|
||||||
|
let mockWorkflowService: Partial<CorrespondenceWorkflowService>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
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({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [CorrespondenceController],
|
controllers: [CorrespondenceController],
|
||||||
}).compile();
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CorrespondenceService,
|
||||||
|
useValue: mockCorrespondenceService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CorrespondenceWorkflowService,
|
||||||
|
useValue: mockWorkflowService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideGuard(JwtAuthGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.overrideGuard(RbacGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.compile();
|
||||||
|
|
||||||
controller = module.get<CorrespondenceController>(CorrespondenceController);
|
controller = module.get<CorrespondenceController>(CorrespondenceController);
|
||||||
});
|
});
|
||||||
@@ -15,4 +50,68 @@ describe('CorrespondenceController', () => {
|
|||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
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,91 +5,209 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
Param, // <--- ✅ 1. เพิ่ม Param
|
Param,
|
||||||
ParseIntPipe, // <--- ✅ 2. เพิ่ม ParseIntPipe
|
ParseIntPipe,
|
||||||
|
Query,
|
||||||
|
Delete,
|
||||||
|
Put,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CorrespondenceService } from './correspondence.service.js';
|
import {
|
||||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
ApiTags,
|
||||||
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto.js'; // <--- ✅ 3. เพิ่ม Import DTO นี้
|
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 { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from '../../common/guards/rbac.guard.js';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { Audit } from '../../common/decorators/audit.decorator';
|
||||||
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')
|
@Controller('correspondences')
|
||||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
export class CorrespondenceController {
|
export class CorrespondenceController {
|
||||||
constructor(private readonly correspondenceService: CorrespondenceService) {}
|
constructor(
|
||||||
|
private readonly correspondenceService: CorrespondenceService,
|
||||||
|
private readonly workflowService: CorrespondenceWorkflowService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post(':id/workflow/action')
|
@Post(':id/workflow/action')
|
||||||
@RequirePermission('workflow.action_review') // สิทธิ์ในการกดอนุมัติ/ตรวจสอบ
|
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
|
||||||
|
@RequirePermission('workflow.action_review')
|
||||||
processAction(
|
processAction(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
|
||||||
@Body() actionDto: WorkflowActionDto,
|
@Body() actionDto: WorkflowActionDto,
|
||||||
@Request() req: any,
|
@Request()
|
||||||
|
req: Request & {
|
||||||
|
user: {
|
||||||
|
user_id: number;
|
||||||
|
assignments?: Array<{ role: { roleName: string } }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.processAction(id, actionDto, req.user);
|
// 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 },
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
|
@ApiOperation({ summary: 'Create new correspondence' })
|
||||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
@ApiResponse({
|
||||||
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
|
status: 201,
|
||||||
return this.correspondenceService.create(createDto, req.user);
|
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]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ปรับปรุง findAll ให้รับ Query Params
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Search correspondences' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Return list of correspondences.' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findAll(@Query() searchDto: SearchCorrespondenceDto) {
|
findAll(@Query() searchDto: SearchCorrespondenceDto) {
|
||||||
return this.correspondenceService.findAll(searchDto);
|
return this.correspondenceService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ เพิ่ม Endpoint นี้ครับ
|
|
||||||
@Post(':id/submit')
|
@Post(':id/submit')
|
||||||
@RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้
|
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
||||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Correspondence submitted successfully.',
|
||||||
|
})
|
||||||
|
@RequirePermission('correspondence.create')
|
||||||
|
@Audit('correspondence.submit', 'correspondence')
|
||||||
submit(
|
submit(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body() submitDto: SubmitCorrespondenceDto,
|
@Body() submitDto: SubmitCorrespondenceDto,
|
||||||
@Request() req: any,
|
@Request()
|
||||||
|
req: Request & {
|
||||||
|
user: {
|
||||||
|
user_id: number;
|
||||||
|
assignments?: Array<{ role: { roleName: string } }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.submit(
|
// 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(
|
||||||
id,
|
id,
|
||||||
submitDto.templateId,
|
req.user.user_id,
|
||||||
req.user,
|
userRoles,
|
||||||
|
submitDto.note
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REFERENCES ---
|
@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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id/references')
|
@Get(':id/references')
|
||||||
|
@ApiOperation({ summary: 'Get referenced documents' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Return list of referenced documents.',
|
||||||
|
})
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getReferences(@Param('id', ParseIntPipe) id: number) {
|
getReferences(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.correspondenceService.getReferences(id);
|
return this.correspondenceService.getReferences(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/references')
|
@Post(':id/references')
|
||||||
@RequirePermission('document.edit') // ต้องมีสิทธิ์แก้ไขถึงจะเพิ่ม Ref ได้
|
@ApiOperation({ summary: 'Add reference to another document' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
||||||
|
@RequirePermission('document.edit')
|
||||||
addReference(
|
addReference(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body() dto: AddReferenceDto,
|
@Body() dto: AddReferenceDto
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.addReference(id, dto);
|
return this.correspondenceService.addReference(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id/references/:targetId')
|
@Delete(':id/references/:targetId')
|
||||||
|
@ApiOperation({ summary: 'Remove reference' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||||
@RequirePermission('document.edit')
|
@RequirePermission('document.edit')
|
||||||
removeReference(
|
removeReference(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Param('targetId', ParseIntPipe) targetId: number,
|
@Param('targetId', ParseIntPipe) targetId: number
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.removeReference(id, targetId);
|
return this.correspondenceService.removeReference(id, targetId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CorrespondenceController } from './correspondence.controller.js';
|
import { CorrespondenceController } from './correspondence.controller';
|
||||||
import { CorrespondenceService } from './correspondence.service.js';
|
import { CorrespondenceService } from './correspondence.service';
|
||||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||||
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';
|
|
||||||
|
|
||||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module.js'; // ต้องใช้ตอน Create
|
// Entities
|
||||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
import { Correspondence } from './entities/correspondence.entity';
|
||||||
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
|
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||||
// Controllers & Services
|
import { Organization } from '../organization/entities/organization.entity';
|
||||||
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
@@ -27,19 +33,18 @@ import { CorrespondenceWorkflowService } from './correspondence-workflow.service
|
|||||||
CorrespondenceRevision,
|
CorrespondenceRevision,
|
||||||
CorrespondenceType,
|
CorrespondenceType,
|
||||||
CorrespondenceStatus,
|
CorrespondenceStatus,
|
||||||
RoutingTemplate, // <--- ลงทะเบียน
|
CorrespondenceReference,
|
||||||
RoutingTemplateStep, // <--- ลงทะเบียน
|
CorrespondenceRecipient,
|
||||||
CorrespondenceRouting, // <--- ลงทะเบียน
|
Organization,
|
||||||
CorrespondenceReference, // <--- ลงทะเบียน
|
|
||||||
]),
|
]),
|
||||||
DocumentNumberingModule, // Import เพื่อขอเลขที่เอกสาร
|
DocumentNumberingModule,
|
||||||
JsonSchemaModule, // Import เพื่อ Validate JSON
|
JsonSchemaModule,
|
||||||
UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้
|
UserModule,
|
||||||
WorkflowEngineModule, // <--- Import WorkflowEngine
|
WorkflowEngineModule,
|
||||||
SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่
|
SearchModule,
|
||||||
],
|
],
|
||||||
controllers: [CorrespondenceController],
|
controllers: [CorrespondenceController],
|
||||||
providers: [CorrespondenceService, CorrespondenceWorkflowService],
|
providers: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||||
exports: [CorrespondenceService],
|
exports: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||||
})
|
})
|
||||||
export class CorrespondenceModule {}
|
export class CorrespondenceModule {}
|
||||||
|
|||||||
@@ -1,18 +1,270 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { CorrespondenceService } from './correspondence.service';
|
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', () => {
|
describe('CorrespondenceService', () => {
|
||||||
let service: 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 () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [CorrespondenceService],
|
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() },
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<CorrespondenceService>(CorrespondenceService);
|
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', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
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,34 +9,38 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, DataSource, Like, In } from 'typeorm';
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
|
||||||
// Entitie
|
// Entities
|
||||||
import { Correspondence } from './entities/correspondence.entity';
|
import { Correspondence } from './entities/correspondence.entity';
|
||||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||||
import { CorrespondenceStatus } from './entities/correspondence-status.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 { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||||
|
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
|
import { Organization } from '../organization/entities/organization.entity';
|
||||||
|
|
||||||
// DTOs
|
// DTOs
|
||||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||||
|
import { DeepPartial } from 'typeorm';
|
||||||
// Interfaces & Enums
|
|
||||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { SearchService } from '../search/search.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()
|
@Injectable()
|
||||||
export class CorrespondenceService {
|
export class CorrespondenceService {
|
||||||
private readonly logger = new Logger(CorrespondenceService.name);
|
private readonly logger = new Logger(CorrespondenceService.name);
|
||||||
@@ -50,12 +54,10 @@ export class CorrespondenceService {
|
|||||||
private typeRepo: Repository<CorrespondenceType>,
|
private typeRepo: Repository<CorrespondenceType>,
|
||||||
@InjectRepository(CorrespondenceStatus)
|
@InjectRepository(CorrespondenceStatus)
|
||||||
private statusRepo: Repository<CorrespondenceStatus>,
|
private statusRepo: Repository<CorrespondenceStatus>,
|
||||||
@InjectRepository(RoutingTemplate)
|
|
||||||
private templateRepo: Repository<RoutingTemplate>,
|
|
||||||
@InjectRepository(CorrespondenceRouting)
|
|
||||||
private routingRepo: Repository<CorrespondenceRouting>,
|
|
||||||
@InjectRepository(CorrespondenceReference)
|
@InjectRepository(CorrespondenceReference)
|
||||||
private referenceRepo: Repository<CorrespondenceReference>,
|
private referenceRepo: Repository<CorrespondenceReference>,
|
||||||
|
@InjectRepository(Organization)
|
||||||
|
private orgRepo: Repository<Organization>,
|
||||||
|
|
||||||
private numberingService: DocumentNumberingService,
|
private numberingService: DocumentNumberingService,
|
||||||
private jsonSchemaService: JsonSchemaService,
|
private jsonSchemaService: JsonSchemaService,
|
||||||
@@ -111,9 +113,9 @@ export class CorrespondenceService {
|
|||||||
if (createDto.details) {
|
if (createDto.details) {
|
||||||
try {
|
try {
|
||||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Schema validation warning for ${type.typeCode}: ${error.message}`
|
`Schema validation warning for ${type.typeCode}: ${(error as Error).message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,24 +127,38 @@ export class CorrespondenceService {
|
|||||||
try {
|
try {
|
||||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Organization Entity
|
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Organization Entity
|
||||||
|
|
||||||
// [FIXED] เรียกใช้แบบ Object Context ตาม Requirement 6B
|
// [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;
|
||||||
|
}
|
||||||
|
|
||||||
const docNumber = await this.numberingService.generateNextNumber({
|
const docNumber = await this.numberingService.generateNextNumber({
|
||||||
projectId: createDto.projectId,
|
projectId: createDto.projectId,
|
||||||
originatorId: userOrgId,
|
originatorOrganizationId: userOrgId,
|
||||||
typeId: createDto.typeId,
|
typeId: createDto.typeId,
|
||||||
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
|
disciplineId: createDto.disciplineId,
|
||||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
subTypeId: createDto.subTypeId,
|
||||||
|
recipientOrganizationId, // [v1.5.1] Pass recipient for document number format
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
customTokens: {
|
customTokens: {
|
||||||
TYPE_CODE: type.typeCode,
|
TYPE_CODE: type.typeCode,
|
||||||
ORG_CODE: orgCode,
|
ORG_CODE: orgCode,
|
||||||
|
RECIPIENT_CODE: recipientCode,
|
||||||
|
REC_CODE: recipientCode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||||
correspondenceNumber: docNumber,
|
correspondenceNumber: docNumber.number,
|
||||||
correspondenceTypeId: createDto.typeId,
|
correspondenceTypeId: createDto.typeId,
|
||||||
disciplineId: createDto.disciplineId, // บันทึก Discipline ลง DB
|
disciplineId: createDto.disciplineId,
|
||||||
projectId: createDto.projectId,
|
projectId: createDto.projectId,
|
||||||
originatorId: userOrgId,
|
originatorId: userOrgId,
|
||||||
isInternal: createDto.isInternal || false,
|
isInternal: createDto.isInternal || false,
|
||||||
@@ -156,16 +172,32 @@ export class CorrespondenceService {
|
|||||||
revisionLabel: 'A',
|
revisionLabel: 'A',
|
||||||
isCurrent: true,
|
isCurrent: true,
|
||||||
statusId: statusDraft.id,
|
statusId: statusDraft.id,
|
||||||
title: createDto.title,
|
subject: createDto.subject,
|
||||||
|
body: createDto.body,
|
||||||
|
remarks: createDto.remarks,
|
||||||
|
dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined,
|
||||||
description: createDto.description,
|
description: createDto.description,
|
||||||
details: createDto.details,
|
details: createDto.details,
|
||||||
createdBy: user.user_id,
|
createdBy: user.user_id,
|
||||||
|
schemaVersion: 1,
|
||||||
});
|
});
|
||||||
await queryRunner.manager.save(revision);
|
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();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
// [NEW V1.5.1] Start Workflow Instance (After Commit)
|
// Start Workflow Instance (non-blocking)
|
||||||
try {
|
try {
|
||||||
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
||||||
await this.workflowEngine.createInstance(
|
await this.workflowEngine.createInstance(
|
||||||
@@ -181,16 +213,15 @@ export class CorrespondenceService {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
`Workflow not started for ${docNumber.number} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||||
);
|
);
|
||||||
// Non-blocking: Document is created, but workflow might not be active.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchService.indexDocument({
|
this.searchService.indexDocument({
|
||||||
id: savedCorr.id,
|
id: savedCorr.id,
|
||||||
type: 'correspondence',
|
type: 'correspondence',
|
||||||
docNumber: docNumber,
|
docNumber: docNumber.number,
|
||||||
title: createDto.title,
|
title: createDto.subject,
|
||||||
description: createDto.description,
|
description: createDto.description,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
projectId: createDto.projectId,
|
projectId: createDto.projectId,
|
||||||
@@ -212,17 +243,35 @@ export class CorrespondenceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (method อื่นๆ คงเดิม)
|
|
||||||
async findAll(searchDto: SearchCorrespondenceDto = {}) {
|
async findAll(searchDto: SearchCorrespondenceDto = {}) {
|
||||||
const { search, typeId, projectId, statusId } = searchDto;
|
const {
|
||||||
|
search,
|
||||||
|
typeId,
|
||||||
|
projectId,
|
||||||
|
statusId,
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
} = searchDto;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const query = this.correspondenceRepo
|
// Change: Query from Revision Repo
|
||||||
.createQueryBuilder('corr')
|
const query = this.revisionRepo
|
||||||
.leftJoinAndSelect('corr.revisions', 'rev')
|
.createQueryBuilder('rev')
|
||||||
|
.leftJoinAndSelect('rev.correspondence', 'corr')
|
||||||
.leftJoinAndSelect('corr.type', 'type')
|
.leftJoinAndSelect('corr.type', 'type')
|
||||||
.leftJoinAndSelect('corr.project', 'project')
|
.leftJoinAndSelect('corr.project', 'project')
|
||||||
.leftJoinAndSelect('corr.originator', 'org')
|
.leftJoinAndSelect('corr.originator', 'org')
|
||||||
.where('rev.isCurrent = :isCurrent', { isCurrent: true });
|
.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
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
query.andWhere('corr.projectId = :projectId', { projectId });
|
query.andWhere('corr.projectId = :projectId', { projectId });
|
||||||
@@ -238,14 +287,25 @@ export class CorrespondenceService {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query.andWhere(
|
query.andWhere(
|
||||||
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
|
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)',
|
||||||
{ search: `%${search}%` }
|
{ search: `%${search}%` }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
query.orderBy('corr.createdAt', 'DESC');
|
// Default Sort: Latest Created
|
||||||
|
query.orderBy('rev.createdAt', 'DESC').skip(skip).take(limit);
|
||||||
|
|
||||||
return query.getMany();
|
const [items, total] = await query.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: items,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number) {
|
||||||
@@ -257,6 +317,8 @@ export class CorrespondenceService {
|
|||||||
'type',
|
'type',
|
||||||
'project',
|
'project',
|
||||||
'originator',
|
'originator',
|
||||||
|
'recipients',
|
||||||
|
'recipients.recipientOrganization', // [v1.5.1] Fixed relation name
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,182 +328,6 @@ export class CorrespondenceService {
|
|||||||
return correspondence;
|
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) {
|
async addReference(id: number, dto: AddReferenceDto) {
|
||||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||||
const target = await this.correspondenceRepo.findOne({
|
const target = await this.correspondenceRepo.findOne({
|
||||||
@@ -499,4 +385,236 @@ export class CorrespondenceService {
|
|||||||
|
|
||||||
return { outgoing, incoming };
|
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,6 +1,11 @@
|
|||||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class AddReferenceDto {
|
export class AddReferenceDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Target Correspondence ID to reference',
|
||||||
|
example: 20,
|
||||||
|
})
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
targetId!: number;
|
targetId!: number;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// File: src/modules/correspondence/dto/create-correspondence.dto.ts
|
|
||||||
import {
|
import {
|
||||||
IsInt,
|
IsInt,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -6,43 +5,99 @@ import {
|
|||||||
IsOptional,
|
IsOptional,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsObject,
|
IsObject,
|
||||||
|
IsDateString,
|
||||||
|
IsArray,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class CreateCorrespondenceDto {
|
export class CreateCorrespondenceDto {
|
||||||
|
@ApiProperty({ description: 'Project ID', example: 1 })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
projectId!: number;
|
projectId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Document Type ID', example: 1 })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
typeId!: number; // ID ของประเภทเอกสาร (เช่น RFA, LETTER)
|
typeId!: number; // ID ของประเภทเอกสาร (เช่น RFA, LETTER)
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Discipline ID', example: 2 })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
disciplineId?: number; // [Req 6B] สาขางาน (เช่น GEN, STR)
|
disciplineId?: number; // [Req 6B] สาขางาน (เช่น GEN, STR)
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Sub Type ID', example: 3 })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
subTypeId?: number; // [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA)
|
subTypeId?: number; // [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA)
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Correspondence Subject',
|
||||||
|
example: 'Monthly Progress Report',
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
title!: string;
|
subject!: 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()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Additional details (JSON)',
|
||||||
|
example: { key: 'value' },
|
||||||
|
})
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
|
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Is internal document?', default: false })
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isInternal?: boolean;
|
isInternal?: boolean;
|
||||||
|
|
||||||
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
|
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Originator Organization ID (for impersonation)',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
}
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Recipients',
|
||||||
|
example: [{ organizationId: 1, type: 'TO' }],
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,52 @@
|
|||||||
import { IsOptional, IsString, IsInt } from 'class-validator';
|
import { IsOptional, IsString, IsInt } from 'class-validator';
|
||||||
import { Type } from 'class-transformer'; // <--- ✅ Import จาก class-transformer
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class SearchCorrespondenceDto {
|
export class SearchCorrespondenceDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Search term (Title or Document Number)',
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
search?: string; // ค้นหาจาก Title หรือ Number
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Filter by Document Type ID' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
typeId?: number;
|
typeId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Filter by Project ID' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
|
|
||||||
// status อาจจะซับซ้อนหน่อยเพราะอยู่ที่ Revision แต่ใส่ไว้ก่อน
|
@ApiPropertyOptional({ description: 'Filter by Status ID' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
statusId?: number;
|
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,7 +1,16 @@
|
|||||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for submitting correspondence to workflow
|
||||||
|
* Uses Unified Workflow Engine - no templateId required
|
||||||
|
*/
|
||||||
export class SubmitCorrespondenceDto {
|
export class SubmitCorrespondenceDto {
|
||||||
@IsInt()
|
@ApiPropertyOptional({
|
||||||
@IsNotEmpty()
|
description: 'Optional note for the submission',
|
||||||
templateId!: number;
|
example: 'Submitting for review',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateCorrespondenceDto } from './create-correspondence.dto';
|
||||||
|
|
||||||
|
export class UpdateCorrespondenceDto extends PartialType(
|
||||||
|
CreateCorrespondenceDto
|
||||||
|
) {}
|
||||||
@@ -1,15 +1,61 @@
|
|||||||
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
|
import { IsEnum, IsString, IsOptional, IsUUID, IsInt } from 'class-validator';
|
||||||
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
|
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for processing workflow actions
|
||||||
|
*
|
||||||
|
* Supports both:
|
||||||
|
* - New Unified Workflow Engine (uses instanceId)
|
||||||
|
* - Legacy RFA workflow (uses returnToSequence)
|
||||||
|
*/
|
||||||
export class WorkflowActionDto {
|
export class WorkflowActionDto {
|
||||||
@IsEnum(WorkflowAction)
|
@ApiPropertyOptional({
|
||||||
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
|
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;
|
||||||
|
|
||||||
|
@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()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
comments?: string;
|
comments?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Sequence to return to (only for RETURN action in legacy RFA)',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
returnToSequence?: number; // ใช้กรณี action = RETURN
|
returnToSequence?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Additional payload data',
|
||||||
|
example: { priority: 'HIGH' },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
payload?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
import { Correspondence } from './correspondence.entity.js';
|
import { Correspondence } from './correspondence.entity';
|
||||||
|
|
||||||
@Entity('correspondence_references')
|
@Entity('correspondence_references')
|
||||||
export class CorrespondenceReference {
|
export class CorrespondenceReference {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Index,
|
Index,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Correspondence } from './correspondence.entity.js';
|
import { Correspondence } from './correspondence.entity';
|
||||||
import { CorrespondenceStatus } from './correspondence-status.entity.js';
|
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||||
import { User } from '../../user/entities/user.entity.js';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
|
||||||
@Entity('correspondence_revisions')
|
@Entity('correspondence_revisions')
|
||||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||||
@@ -35,15 +35,24 @@ export class CorrespondenceRevision {
|
|||||||
@Column({ name: 'correspondence_status_id' })
|
@Column({ name: 'correspondence_status_id' })
|
||||||
statusId!: number;
|
statusId!: number;
|
||||||
|
|
||||||
@Column({ length: 255 })
|
@Column({ length: 500 })
|
||||||
title!: string;
|
subject!: string;
|
||||||
|
|
||||||
@Column({ name: 'description', type: 'text', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
body?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
remarks?: string;
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true })
|
@Column({ type: 'json', nullable: true })
|
||||||
details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type
|
details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type
|
||||||
|
|
||||||
|
@Column({ name: 'schema_version', default: 1 })
|
||||||
|
schemaVersion!: number;
|
||||||
|
|
||||||
// ✅ [New] Virtual Column: ดึง Project ID จาก JSON details
|
// ✅ [New] Virtual Column: ดึง Project ID จาก JSON details
|
||||||
@Column({
|
@Column({
|
||||||
name: 'v_ref_project_id',
|
name: 'v_ref_project_id',
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||||
import { Organization } from '../../project/entities/organization.entity.js';
|
import { Organization } from '../../organization/entities/organization.entity';
|
||||||
import { User } from '../../user/entities/user.entity.js';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { RoutingTemplate } from './routing-template.entity.js';
|
import { RoutingTemplate } from './routing-template.entity';
|
||||||
|
|
||||||
@Entity('correspondence_routings')
|
@Entity('correspondence_routings')
|
||||||
export class CorrespondenceRouting {
|
export class CorrespondenceRouting {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Contract } from '../../project/entities/contract.entity'; // ปรับ path ตามจริง
|
import { Contract } from '../../contract/entities/contract.entity'; // ปรับ path ตามจริง
|
||||||
import { CorrespondenceType } from './correspondence-type.entity'; // ปรับ path ตามจริง
|
import { CorrespondenceType } from './correspondence-type.entity'; // ปรับ path ตามจริง
|
||||||
|
|
||||||
@Entity('correspondence_sub_types')
|
@Entity('correspondence_sub_types')
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import {
|
|||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Project } from '../../project/entities/project.entity.js';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
import { Organization } from '../../project/entities/organization.entity.js';
|
import { Organization } from '../../organization/entities/organization.entity';
|
||||||
import { CorrespondenceType } from './correspondence-type.entity.js';
|
import { CorrespondenceType } from './correspondence-type.entity';
|
||||||
import { User } from '../../user/entities/user.entity.js';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js'; // เดี๋ยวสร้าง
|
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
||||||
|
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||||
|
import { Discipline } from '../../master/entities/discipline.entity';
|
||||||
|
|
||||||
@Entity('correspondences')
|
@Entity('correspondences')
|
||||||
export class Correspondence {
|
export class Correspondence {
|
||||||
@@ -68,9 +70,9 @@ export class Correspondence {
|
|||||||
creator?: User;
|
creator?: User;
|
||||||
|
|
||||||
// [New V1.5.1]
|
// [New V1.5.1]
|
||||||
@ManyToOne('Discipline')
|
@ManyToOne(() => Discipline)
|
||||||
@JoinColumn({ name: 'discipline_id' })
|
@JoinColumn({ name: 'discipline_id' })
|
||||||
discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import.
|
discipline?: Discipline;
|
||||||
|
|
||||||
// One Correspondence has Many Revisions
|
// One Correspondence has Many Revisions
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
@@ -78,4 +80,11 @@ export class Correspondence {
|
|||||||
(revision) => revision.correspondence
|
(revision) => revision.correspondence
|
||||||
)
|
)
|
||||||
revisions?: CorrespondenceRevision[];
|
revisions?: CorrespondenceRevision[];
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => CorrespondenceRecipient,
|
||||||
|
(recipient) => recipient.correspondence,
|
||||||
|
{ cascade: true }
|
||||||
|
)
|
||||||
|
recipients?: CorrespondenceRecipient[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
// File: src/modules/correspondence/entities/routing-template-step.entity.ts
|
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
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')
|
@Entity('correspondence_routing_template_steps')
|
||||||
export class RoutingTemplateStep {
|
export class RoutingTemplateStep {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@@ -24,27 +21,12 @@ export class RoutingTemplateStep {
|
|||||||
@Column({ name: 'to_organization_id' })
|
@Column({ name: 'to_organization_id' })
|
||||||
toOrganizationId!: number;
|
toOrganizationId!: number;
|
||||||
|
|
||||||
@Column({ name: 'role_id', nullable: true })
|
@Column({ name: 'step_purpose', length: 50, default: 'FOR_REVIEW' })
|
||||||
roleId?: number;
|
|
||||||
|
|
||||||
@Column({ name: 'step_purpose', default: 'FOR_REVIEW' })
|
|
||||||
stepPurpose!: string;
|
stepPurpose!: string;
|
||||||
|
|
||||||
@Column({ name: 'expected_days', nullable: true })
|
@Column({ name: 'expected_days', default: 7 })
|
||||||
expectedDays?: number;
|
expectedDays!: number;
|
||||||
|
|
||||||
// Relations
|
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||||
@ManyToOne(() => RoutingTemplate, (template) => template.steps, {
|
// template?: RoutingTemplate;
|
||||||
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,7 +1,12 @@
|
|||||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
import { Entity, Column, PrimaryGeneratedColumn } 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')
|
@Entity('correspondence_routing_templates')
|
||||||
export class RoutingTemplate {
|
export class RoutingTemplate {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@@ -14,14 +19,14 @@ export class RoutingTemplate {
|
|||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@Column({ name: 'project_id', nullable: true })
|
@Column({ name: 'project_id', nullable: true })
|
||||||
projectId?: number; // NULL = แม่แบบทั่วไป
|
projectId?: number;
|
||||||
|
|
||||||
@Column({ name: 'is_active', default: true })
|
@Column({ name: 'is_active', default: true })
|
||||||
isActive!: boolean;
|
isActive!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true, name: 'workflow_config' })
|
@Column({ type: 'json', nullable: true, name: 'workflow_config' })
|
||||||
workflowConfig?: any;
|
workflowConfig?: Record<string, unknown>;
|
||||||
|
|
||||||
@OneToMany(() => RoutingTemplateStep, (step) => step.template)
|
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||||
steps?: RoutingTemplateStep[];
|
// steps?: RoutingTemplateStep[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
51
backend/src/modules/dashboard/dashboard.controller.ts
Normal file
51
backend/src/modules/dashboard/dashboard.controller.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/src/modules/dashboard/dashboard.module.ts
Normal file
24
backend/src/modules/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// 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 {}
|
||||||
214
backend/src/modules/dashboard/dashboard.service.ts
Normal file
214
backend/src/modules/dashboard/dashboard.service.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// 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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
backend/src/modules/dashboard/dto/dashboard-stats.dto.ts
Normal file
27
backend/src/modules/dashboard/dto/dashboard-stats.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
42
backend/src/modules/dashboard/dto/get-activity.dto.ts
Normal file
42
backend/src/modules/dashboard/dto/get-activity.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
55
backend/src/modules/dashboard/dto/get-pending.dto.ts
Normal file
55
backend/src/modules/dashboard/dto/get-pending.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
6
backend/src/modules/dashboard/dto/index.ts
Normal file
6
backend/src/modules/dashboard/dto/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// File: src/modules/dashboard/dto/index.ts
|
||||||
|
// บันทึกการแก้ไข: สร้างใหม่สำหรับ export DTOs ทั้งหมด
|
||||||
|
|
||||||
|
export * from './dashboard-stats.dto';
|
||||||
|
export * from './get-activity.dto';
|
||||||
|
export * from './get-pending.dto';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user