Compare commits
20 Commits
fc6cf11818
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
||||
|
||||
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.
|
||||
- **Consult `specs/database/data-dictionary-v1.5.1.md`** for field meanings and business rules.
|
||||
- **Check `specs/database/lcbp3-v1.5.1-seed.sql`** to understand initial data states.
|
||||
- *Action:* - **Read `specs/07-database/lcbp3-v1.6.0-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints.
|
||||
- **Consult `specs/07-database/data-dictionary-v1.6.0.md`** for field meanings and business rules.
|
||||
- **Check `specs/07-database/lcbp3-v1.6.0-seed.sql`** to understand initial data states.
|
||||
- *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here.
|
||||
|
||||
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
|
||||
|
||||
## 🧠 Role & Persona
|
||||
@@ -14,8 +18,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
||||
|
||||
## 💻 Tech Stack & Constraints
|
||||
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis (BullMQ).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI.
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, React Context / Zustand, React Hook Form + Zod, Axios.
|
||||
- **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
|
||||
|
||||
## 🛡️ Security & Integrity Rules
|
||||
@@ -27,8 +31,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
||||
|
||||
## workflow Guidelines
|
||||
|
||||
- When implementing **Workflow Engine**, strictly follow the **DSL** design in `2_Backend_Plan_V1_4_4.Phase6A.md`.
|
||||
- Always verify database schema against `4_Data_Dictionary_V1_4_4.md` before writing queries.
|
||||
- When implementing strictly follow the documents in `specs/`.
|
||||
- Always verify database schema against `specs/07-database/` before writing queries.
|
||||
|
||||
## 🚫 Forbidden Actions
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
backend/node_modules/
|
||||
frontend/node_modules/
|
||||
dist
|
||||
build
|
||||
*.min.js
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -33,6 +33,8 @@
|
||||
"alefragnani.bookmarks",
|
||||
"pkief.material-icon-theme",
|
||||
"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")
|
||||
|
||||
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$CommitMsg = "Backup: $Message | $Timestamp"
|
||||
$Timestamp = Get-Date -Format "yyMMdd:HHmm"
|
||||
$CommitMsg = "$Timestamp $Message"
|
||||
|
||||
Write-Host "Backup: $CommitMsg" -ForegroundColor Cyan
|
||||
Write-Host "📦 $CommitMsg" -ForegroundColor Cyan
|
||||
|
||||
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
|
||||
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -1,13 +1,145 @@
|
||||
# Version history
|
||||
# Version History
|
||||
|
||||
## 1.4.5 (2025-11-28)
|
||||
## [Unreleased]
|
||||
|
||||
### In Progress
|
||||
- E2E Testing & UAT preparation
|
||||
- Performance optimization and load testing
|
||||
- Production deployment preparation
|
||||
|
||||
## 1.6.0 (2025-12-13)
|
||||
|
||||
### Summary
|
||||
**Schema Refactoring Release** - Major restructuring of correspondence and RFA tables for improved data consistency.
|
||||
|
||||
- Backend development 80% remaining test tasks
|
||||
### 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)
|
||||
|
||||
### Summary
|
||||
Initial spec-kit structure establishment and documentation organization.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the version to 1.5.0
|
||||
|
||||
@@ -20,18 +20,19 @@
|
||||
|
||||
## 🗂️ Specification Structure
|
||||
|
||||
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 6 หมวดหลัก:
|
||||
โครงสร้างเอกสาร Specifications ของโครงการแบ่งออกเป็น 9 หมวดหลัก:
|
||||
|
||||
```
|
||||
specs/
|
||||
├── 00-overview/ # ภาพรวมโครงการ
|
||||
│ ├── README.md # Project overview
|
||||
│ └── glossary.md # คำศัพท์เทคนิค
|
||||
├── 00-overview/ # ภาพรวมโครงการ (3 docs)
|
||||
│ ├── README.md # Project overview
|
||||
│ ├── glossary.md # คำศัพท์เทคนิค
|
||||
│ └── quick-start.md # Quick start guide
|
||||
│
|
||||
├── 01-requirements/ # ข้อกำหนดระบบ
|
||||
│ ├── README.md # Requirements overview
|
||||
│ ├── 01-objectives.md # วัตถุประสงค์
|
||||
│ ├── 02-architecture.md # สถาปัตยกรรม
|
||||
├── 01-requirements/ # ข้อกำหนดระบบ (21 docs)
|
||||
│ ├── README.md # Requirements overview
|
||||
│ ├── 01-objectives.md # วัตถุประสงค์
|
||||
│ ├── 02-architecture.md # สถาปัตยกรรม
|
||||
│ ├── 03-functional-requirements.md
|
||||
│ ├── 03.1-project-management.md
|
||||
│ ├── 03.2-correspondence.md
|
||||
@@ -50,39 +51,59 @@ specs/
|
||||
│ ├── 06-non-functional.md
|
||||
│ └── 07-testing.md
|
||||
│
|
||||
├── 02-architecture/ # สถาปัตยกรรมระบบ
|
||||
├── 02-architecture/ # สถาปัตยกรรมระบบ (4 docs)
|
||||
│ ├── README.md
|
||||
│ ├── system-architecture.md
|
||||
│ ├── api-design.md
|
||||
│ └── data-model.md
|
||||
│
|
||||
├── 03-implementation/ # แผนการพัฒนา
|
||||
├── 03-implementation/ # แผนการพัฒนา (5 docs)
|
||||
│ ├── README.md
|
||||
│ ├── backend-plan.md
|
||||
│ ├── frontend-plan.md
|
||||
│ └── integration-plan.md
|
||||
│ ├── backend-guidelines.md
|
||||
│ ├── frontend-guidelines.md
|
||||
│ ├── testing-strategy.md
|
||||
│ └── code-standards.md
|
||||
│
|
||||
├── 04-operations/ # การดำเนินงาน
|
||||
├── 04-operations/ # การดำเนินงาน (9 docs)
|
||||
│ ├── README.md
|
||||
│ ├── deployment.md
|
||||
│ └── monitoring.md
|
||||
│ ├── monitoring.md
|
||||
│ └── ...
|
||||
│
|
||||
└── 05-decisions/ # Architecture Decision Records
|
||||
├── README.md
|
||||
├── 001-workflow-engine.md
|
||||
└── 002-file-storage.md
|
||||
├── 05-decisions/ # Architecture Decision Records (17 ADRs)
|
||||
│ ├── README.md
|
||||
│ ├── ADR-001-workflow-engine.md
|
||||
│ ├── ADR-002-document-numbering.md
|
||||
│ └── ...
|
||||
│
|
||||
├── 06-tasks/ # Active Tasks & Progress (34 files)
|
||||
│ ├── frontend-progress-report.md
|
||||
│ ├── backend-progress-report.md
|
||||
│ └── ...
|
||||
│
|
||||
├── 07-database/ # Database Schema (8 files)
|
||||
│ ├── lcbp3-v1.5.1-schema.sql
|
||||
│ ├── lcbp3-v1.5.1-seed.sql
|
||||
│ ├── data-dictionary-v1.5.1.md
|
||||
│ └── ...
|
||||
│
|
||||
└── 09-history/ # Archived Implementations (9 files)
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 📋 หมวดหมู่เอกสาร
|
||||
|
||||
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
||||
| หมวด | วัตถุประสงค์ | ผู้ดูแล |
|
||||
| --------------------- | ----------------------------- | ----------------------------- |
|
||||
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
||||
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
||||
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
||||
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
||||
| **00-overview** | ภาพรวมโครงการและคำศัพท์ | Project Manager |
|
||||
| **01-requirements** | ข้อกำหนดฟังก์ชันและระบบ | Business Analyst + Tech Lead |
|
||||
| **02-architecture** | สถาปัตยกรรมและการออกแบบ | Tech Lead + Architects |
|
||||
| **03-implementation** | แผนการพัฒนาและ Implementation | Development Team Leads |
|
||||
| **04-operations** | Deployment และ Operations | DevOps Team |
|
||||
| **05-decisions** | Architecture Decision Records | Tech Lead + Senior Developers |
|
||||
| **06-tasks** | Active Tasks & Progress | All Team Members |
|
||||
| **07-database** | Database Schema & Seed Data | Backend Lead + DBA |
|
||||
| **09-history** | Archived Implementations | Tech Lead |
|
||||
|
||||
---
|
||||
|
||||
@@ -454,11 +475,11 @@ Then [expected result]
|
||||
|
||||
### Review Levels
|
||||
|
||||
| Level | Reviewer | Scope |
|
||||
|-------|----------|-------|
|
||||
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
||||
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
||||
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
||||
| Level | Reviewer | Scope |
|
||||
| ------------------------ | --------------- | ------------------------------- |
|
||||
| **L1: Peer Review** | Team Member | Format, Clarity, Completeness |
|
||||
| **L2: Technical Review** | Tech Lead | Technical Accuracy, Feasibility |
|
||||
| **L3: Approval** | Project Manager | Business Alignment, Impact |
|
||||
|
||||
### Review Timeline
|
||||
|
||||
|
||||
194
README.md
194
README.md
@@ -4,9 +4,23 @@
|
||||
>
|
||||
> ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
||||
|
||||
[](./CHANGELOG.md)
|
||||
[](./CHANGELOG.md)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 📈 Current Status (As of 2025-12-13)
|
||||
|
||||
**Overall Progress: ~95% Feature Complete - Production Ready**
|
||||
|
||||
- ✅ **Backend**: All 18 core modules implemented (~95%)
|
||||
- ✅ **Frontend**: All 15 UI tasks completed (100%)
|
||||
- ✅ **Database**: Schema v1.6.0 active with complete seed data
|
||||
- ✅ **Documentation**: Comprehensive specs/ at v1.6.0
|
||||
- ✅ **Admin Tools**: Workflow & Numbering configuration UIs complete
|
||||
- 🔄 **Testing**: E2E tests and UAT in progress
|
||||
- 📋 **Next**: Production deployment preparation
|
||||
|
||||
---
|
||||
|
||||
@@ -41,7 +55,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
||||
```typescript
|
||||
{
|
||||
"framework": "NestJS (TypeScript, ESM)",
|
||||
"database": "MariaDB 10.11",
|
||||
"database": "MariaDB 11.8",
|
||||
"orm": "TypeORM",
|
||||
"authentication": "JWT + Passport",
|
||||
"authorization": "CASL (RBAC)",
|
||||
@@ -111,7 +125,7 @@ LCBP3-DMS เป็นระบบบริหารจัดการเอก
|
||||
- **Node.js**: v20.x หรือสูงกว่า
|
||||
- **pnpm**: v8.x หรือสูงกว่า
|
||||
- **Docker**: v24.x หรือสูงกว่า
|
||||
- **MariaDB**: 10.11
|
||||
- **MariaDB**: 11.8
|
||||
- **Redis**: 7.x
|
||||
|
||||
### การติดตั้ง
|
||||
@@ -194,46 +208,88 @@ Superadmin:
|
||||
|
||||
```
|
||||
lcbp3-dms/
|
||||
├── backend/ # NestJS Backend
|
||||
├── backend/ # 🔧 NestJS Backend
|
||||
│ ├── src/
|
||||
│ │ ├── common/ # Shared modules
|
||||
│ │ ├── modules/ # Feature modules
|
||||
│ │ │ ├── auth/
|
||||
│ │ │ ├── user/
|
||||
│ │ │ ├── project/
|
||||
│ │ │ ├── correspondence/
|
||||
│ │ │ ├── rfa/
|
||||
│ │ │ ├── drawing/
|
||||
│ │ │ ├── workflow-engine/
|
||||
│ │ │ └── ...
|
||||
│ │ ├── common/ # Shared utilities, guards, decorators
|
||||
│ │ ├── config/ # Configuration module
|
||||
│ │ ├── database/ # Database entities & migrations
|
||||
│ │ ├── modules/ # Feature modules (17 modules)
|
||||
│ │ │ ├── auth/ # JWT Authentication
|
||||
│ │ │ ├── user/ # User management & RBAC
|
||||
│ │ │ ├── project/ # Project & Contract management
|
||||
│ │ │ ├── correspondence/ # Correspondence module
|
||||
│ │ │ ├── rfa/ # Request for Approval
|
||||
│ │ │ ├── drawing/ # Contract & Shop Drawings
|
||||
│ │ │ ├── workflow-engine/# DSL Workflow Engine
|
||||
│ │ │ ├── document-numbering/ # Auto numbering
|
||||
│ │ │ ├── transmittal/ # Transmittal management
|
||||
│ │ │ ├── circulation/ # Circulation sheets
|
||||
│ │ │ ├── search/ # Elasticsearch integration
|
||||
│ │ │ ├── dashboard/ # Statistics & reporting
|
||||
│ │ │ ├── notification/ # Email/LINE notifications
|
||||
│ │ │ ├── monitoring/ # Health checks & metrics
|
||||
│ │ │ ├── master/ # Master data management
|
||||
│ │ │ ├── organizations/ # Organization management
|
||||
│ │ │ └── json-schema/ # JSON Schema validation
|
||||
│ │ └── main.ts
|
||||
│ ├── test/
|
||||
│ ├── test/ # Unit & E2E tests
|
||||
│ └── package.json
|
||||
│
|
||||
├── frontend/ # Next.js Frontend
|
||||
│ ├── app/ # App Router
|
||||
│ ├── components/ # React Components
|
||||
│ ├── lib/ # Utilities
|
||||
├── frontend/ # 🎨 Next.js Frontend
|
||||
│ ├── app/ # App Router
|
||||
│ │ ├── (admin)/ # Admin panel routes
|
||||
│ │ │ └── admin/
|
||||
│ │ │ ├── workflows/ # Workflow configuration
|
||||
│ │ │ ├── numbering/ # Document numbering config
|
||||
│ │ │ ├── users/ # User management
|
||||
│ │ │ └── ...
|
||||
│ │ ├── (auth)/ # Authentication pages
|
||||
│ │ ├── (dashboard)/ # Main dashboard routes
|
||||
│ │ │ ├── correspondences/
|
||||
│ │ │ ├── rfas/
|
||||
│ │ │ ├── drawings/
|
||||
│ │ │ └── ...
|
||||
│ │ └── 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
|
||||
│
|
||||
├── 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)
|
||||
│ ├── 00-overview/ # Project overview & glossary
|
||||
│ ├── 01-requirements/ # Functional requirements
|
||||
│ ├── 02-architecture/ # System architecture & ADRs
|
||||
│ ├── 03-implementation/ # Implementation guidelines
|
||||
│ ├── 04-operations/ # Deployment & operations
|
||||
│ ├── 05-decisions/ # Architecture Decision Records
|
||||
│ ├── 06-tasks/ # Active tasks
|
||||
│ ├── 07-database/ # Database schema & seed data
|
||||
│ └── 09-history/ # Implementation history
|
||||
├── docs/ # 📚 Legacy documentation
|
||||
├── diagrams/ # 📊 Architecture diagrams
|
||||
├── infrastructure/ # 🐳 Docker & Deployment configs
|
||||
│
|
||||
├── infrastructure/ # Docker & Deployment
|
||||
│ └── Markdown/ # Legacy docs
|
||||
│
|
||||
└── pnpm-workspace.yaml
|
||||
├── .gemini/ # 🤖 AI agent configuration
|
||||
├── .agent/ # Agent workflows
|
||||
├── GEMINI.md # AI coding guidelines
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── CHANGELOG.md # Version history
|
||||
└── pnpm-workspace.yaml # Monorepo configuration
|
||||
```
|
||||
|
||||
---
|
||||
@@ -248,16 +304,16 @@ lcbp3-dms/
|
||||
| **Requirements** | ข้อกำหนดระบบและฟังก์ชันการทำงาน | `specs/01-requirements/` |
|
||||
| **Architecture** | สถาปัตยกรรมระบบ, ADRs | `specs/02-architecture/` |
|
||||
| **Implementation** | แนวทางการพัฒนา Backend/Frontend | `specs/03-implementation/` |
|
||||
| **Database** | Schema v1.5.1 + Seed Data | `specs/07-database/` |
|
||||
| **Database** | Schema v1.6.0 + Seed Data | `specs/07-database/` |
|
||||
|
||||
### Schema & Seed Data
|
||||
|
||||
```bash
|
||||
# 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.6.0-schema.sql
|
||||
|
||||
# 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.6.0-seed-basic.sql
|
||||
```
|
||||
|
||||
### Legacy Documentation
|
||||
@@ -466,7 +522,7 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
||||
|
||||
สำหรับคำถามหรือปัญหา กรุณาติดต่อ:
|
||||
|
||||
- **Email**: support@np-dms.work
|
||||
- **Email**: <support@np-dms.work>
|
||||
- **Internal Chat**: [ระบุช่องทาง]
|
||||
- **Issue Tracker**: [Gitea Issues](https://git.np-dms.work/lcbp3/lcbp3-dms/issues)
|
||||
|
||||
@@ -474,26 +530,54 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
### Version 1.5.1 (Current - Dec 2025)
|
||||
### Version 1.5.1 (Current - Dec 2025) ✅ **FEATURE COMPLETE**
|
||||
|
||||
- ✅ Core Infrastructure
|
||||
- ✅ Authentication & Authorization (JWT + CASL RBAC)
|
||||
- ✅ **CASL RBAC 4-Level** - Global, Org, Project, Contract
|
||||
- ✅ **Workflow DSL Parser** - Zod validation & state machine
|
||||
**Backend (18 Modules - ~95%)**
|
||||
- ✅ Core Infrastructure (Auth, RBAC, File Storage)
|
||||
- ✅ Authentication & Authorization (JWT + CASL RBAC 4-Level)
|
||||
- ✅ Correspondence Module (Master-Revision Pattern)
|
||||
- ✅ **Document Number Audit** - Compliance tracking
|
||||
- ✅ **All Token Types** - Including {RECIPIENT}
|
||||
- 🔄 RFA Module
|
||||
- 🔄 Drawing Module
|
||||
- ✅ RFA Module (Full CRUD + Workflow)
|
||||
- ✅ Drawing Module (Contract + Shop Drawings)
|
||||
- ✅ Workflow Engine (DSL-based Hybrid)
|
||||
- ✅ Document Numbering (Redlock + Optimistic Locking)
|
||||
- ✅ Search (Elasticsearch Direct Indexing)
|
||||
- ✅ Transmittal & Circulation Modules
|
||||
- ✅ Notification & Audit Systems
|
||||
- ✅ Master Data Management
|
||||
- ✅ User Management
|
||||
- ✅ Dashboard & Monitoring
|
||||
- ✅ 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
|
||||
- 📊 Dashboard Analytics
|
||||
- 🔔 Enhanced Notifications (LINE/Email)
|
||||
- 🔄 E2E Tests for Critical APIs
|
||||
- 📈 Prometheus Metrics
|
||||
**Documentation**
|
||||
- ✅ Complete specs/ v1.6.0 (21 requirements, 17 ADRs)
|
||||
- ✅ Database Schema v1.6.0 with seed data
|
||||
- ✅ Implementation & Operations Guides
|
||||
|
||||
### Version 1.7.0 (Planned - Q1 2026)
|
||||
|
||||
**Production Enhancements**
|
||||
- 📋 E2E Test Coverage (Playwright/Cypress)
|
||||
- 📊 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)
|
||||
|
||||
**Optional Improvements**
|
||||
- 🤖 AI-powered Document Classification
|
||||
- 📧 Advanced Email Templates
|
||||
- 🔐 SSO Integration (LDAP/Active Directory)
|
||||
- 📦 Bulk Operations & Import/Export Tools
|
||||
|
||||
---
|
||||
|
||||
|
||||
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).
|
||||
|
||||
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",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"doc": "npx @compodoc/compodoc -p tsconfig.doc.json -s",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test": "jest --forceExit",
|
||||
"test:debug-handles": "jest --detectOpenHandles",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
@@ -43,6 +45,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.1.9",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@willsoto/nestjs-prometheus": "^6.0.2",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"async-retry": "^1.3.3",
|
||||
@@ -71,11 +74,12 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.27",
|
||||
"uuid": "^13.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.18.3",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@compodoc/compodoc": "^1.1.32",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
@@ -93,7 +97,7 @@
|
||||
"@types/opossum": "^8.1.9",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"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
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 { UserModule } from './modules/user/user.module';
|
||||
import { ProjectModule } from './modules/project/project.module';
|
||||
import { OrganizationModule } from './modules/organization/organization.module';
|
||||
import { ContractModule } from './modules/contract/contract.module';
|
||||
import { MasterModule } from './modules/master/master.module'; // [NEW] ✅ เพิ่ม MasterModule
|
||||
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
||||
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
||||
@@ -40,9 +42,11 @@ import { DrawingModule } from './modules/drawing/drawing.module';
|
||||
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
||||
import { CirculationModule } from './modules/circulation/circulation.module';
|
||||
import { NotificationModule } from './modules/notification/notification.module';
|
||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
||||
import { ResilienceModule } from './common/resilience/resilience.module';
|
||||
import { SearchModule } from './modules/search/search.module';
|
||||
import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -88,7 +92,7 @@ import { SearchModule } from './modules/search/search.module';
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
@@ -107,7 +111,7 @@ import { SearchModule } from './modules/search/search.module';
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
@@ -136,7 +140,10 @@ import { SearchModule } from './modules/search/search.module';
|
||||
// 📦 Feature Modules
|
||||
AuthModule,
|
||||
UserModule,
|
||||
UserModule,
|
||||
ProjectModule,
|
||||
OrganizationModule,
|
||||
ContractModule,
|
||||
MasterModule, // ✅ [NEW] Register MasterModule here
|
||||
FileStorageModule,
|
||||
DocumentNumberingModule,
|
||||
@@ -149,6 +156,8 @@ import { SearchModule } from './modules/search/search.module';
|
||||
CirculationModule,
|
||||
SearchModule,
|
||||
NotificationModule,
|
||||
DashboardModule,
|
||||
AuditLogModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -1,30 +1,86 @@
|
||||
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
|
||||
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
|
||||
@Post('login')
|
||||
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
);
|
||||
beforeEach(async () => {
|
||||
mockAuthService = {
|
||||
validateUser: jest.fn(),
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: mockAuthService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
return this.authService.login(user);
|
||||
}
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
});
|
||||
|
||||
@Post('register-admin')
|
||||
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
}
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return tokens when credentials are valid', async () => {
|
||||
const loginDto = { username: 'test', password: 'password' };
|
||||
const mockUser = { user_id: 1, username: 'test' };
|
||||
const mockTokens = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
user: mockUser,
|
||||
};
|
||||
|
||||
(mockAuthService.validateUser as jest.Mock).mockResolvedValue(mockUser);
|
||||
(mockAuthService.login as jest.Mock).mockResolvedValue(mockTokens);
|
||||
|
||||
const result = await controller.login(loginDto);
|
||||
|
||||
expect(mockAuthService.validateUser).toHaveBeenCalledWith(
|
||||
'test',
|
||||
'password'
|
||||
);
|
||||
expect(mockAuthService.login).toHaveBeenCalledWith(mockUser);
|
||||
expect(result).toEqual(mockTokens);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException when credentials are invalid', async () => {
|
||||
const loginDto = { username: 'test', password: 'wrong' };
|
||||
(mockAuthService.validateUser as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
await expect(controller.login(loginDto)).rejects.toThrow(
|
||||
UnauthorizedException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
const registerDto = {
|
||||
username: 'newuser',
|
||||
password: 'password',
|
||||
email: 'test@test.com',
|
||||
display_name: 'Test User',
|
||||
};
|
||||
const mockUser = { user_id: 1, ...registerDto };
|
||||
|
||||
(mockAuthService.register as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const result = await controller.register(registerDto);
|
||||
|
||||
expect(mockAuthService.register).toHaveBeenCalledWith(registerDto);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,17 +11,25 @@ import {
|
||||
Req,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js';
|
||||
import { RegisterDto } from './dto/register.dto.js';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard.js';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Request } from 'express'; // ✅ Import Request
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiResponse,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
|
||||
// สร้าง Interface สำหรับ Request ที่มี User (เพื่อให้ TS รู้จัก req.user)
|
||||
// สร้าง Interface สำหรับ Request ที่มี User
|
||||
interface RequestWithUser extends Request {
|
||||
user: any;
|
||||
}
|
||||
@@ -34,11 +42,24 @@ export class AuthController {
|
||||
@Post('login')
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@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) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
loginDto.password
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
@@ -51,7 +72,9 @@ export class AuthController {
|
||||
@Post('register-admin')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@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) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
@@ -59,9 +82,20 @@ export class AuthController {
|
||||
@UseGuards(JwtRefreshGuard)
|
||||
@Post('refresh')
|
||||
@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) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
return this.authService.refreshToken(req.user.sub, req.user.refreshToken);
|
||||
}
|
||||
|
||||
@@ -69,23 +103,51 @@ export class AuthController {
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@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) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
// ต้องเช็คว่ามี token หรือไม่ เพื่อป้องกัน runtime error
|
||||
if (!token) {
|
||||
return { message: 'No token provided' };
|
||||
}
|
||||
// ส่ง refresh token ไปด้วยถ้ามี (ใน header หรือ body)
|
||||
// สำหรับตอนนี้ส่งแค่ access token ไป blacklist
|
||||
return this.authService.logout(req.user.sub, token);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('profile')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'ดึงข้อมูลผู้ใช้ปัจจุบัน' })
|
||||
@ApiOperation({ summary: 'Get current user profile' })
|
||||
@ApiResponse({ status: 200, description: 'User profile' })
|
||||
getProfile(@Req() req: RequestWithUser) {
|
||||
// ✅ ระบุ Type ชัดเจน
|
||||
return req.user;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('sessions')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get active sessions' })
|
||||
@ApiResponse({ status: 200, description: 'List of active sessions' })
|
||||
async getSessions() {
|
||||
return this.authService.getActiveSessions();
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('sessions/:id')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Revoke session' })
|
||||
@ApiResponse({ status: 200, description: 'Session revoked' })
|
||||
async revokeSession(@Param('id') id: string) {
|
||||
return this.authService.revokeSession(parseInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// File: src/common/auth/auth.module.ts
|
||||
// บันทึกการแก้ไข: แก้ไข Type Mismatch ของ expiresIn (Fix TS2322)
|
||||
// [P0-1] เพิ่ม CASL RBAC Integration
|
||||
// [P2-2] Register RefreshToken Entity
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@@ -13,18 +14,19 @@ import { UserModule } from '../../modules/user/user.module.js';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy.js';
|
||||
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy.js';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { CaslModule } from './casl/casl.module'; // [P0-1] Import CASL
|
||||
import { PermissionsGuard } from './guards/permissions.guard'; // [P0-1] Import Guard
|
||||
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||
import { CaslModule } from './casl/casl.module';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User]),
|
||||
TypeOrmModule.forFeature([User, RefreshToken]), // [P2-2] Added RefreshToken
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
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
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
JwtRefreshStrategy,
|
||||
PermissionsGuard, // [P0-1] Register PermissionsGuard
|
||||
CaslModule,
|
||||
],
|
||||
providers: [AuthService, JwtStrategy, JwtRefreshStrategy, PermissionsGuard],
|
||||
controllers: [AuthController],
|
||||
exports: [
|
||||
AuthService,
|
||||
PermissionsGuard, // [P0-1] Export for use in other modules
|
||||
],
|
||||
exports: [AuthService, PermissionsGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,18 +1,201 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
// Mock bcrypt at top level
|
||||
jest.mock('bcrypt', () => ({
|
||||
compare: jest.fn(),
|
||||
hash: jest.fn().mockResolvedValue('hashedpassword'),
|
||||
genSalt: jest.fn().mockResolvedValue('salt'),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
let userService: UserService;
|
||||
let jwtService: JwtService;
|
||||
let tokenRepo: Repository<RefreshToken>;
|
||||
|
||||
const mockUser = {
|
||||
user_id: 1,
|
||||
username: 'testuser',
|
||||
password: 'hashedpassword',
|
||||
primaryOrganizationId: 1,
|
||||
};
|
||||
|
||||
const mockQueryBuilder = {
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue(mockUser),
|
||||
};
|
||||
|
||||
const mockUserRepo = {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
};
|
||||
|
||||
const mockTokenRepo = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset bcrypt mocks
|
||||
bcrypt.compare.mockResolvedValue(true);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
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();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
jwtService = module.get<JwtService>(JwtService);
|
||||
tokenRepo = module.get(getRepositoryToken(RefreshToken));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('validateUser', () => {
|
||||
it('should return user without password if validation succeeds', async () => {
|
||||
const result = await service.validateUser('testuser', 'password');
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toHaveProperty('password');
|
||||
expect(result.username).toBe('testuser');
|
||||
});
|
||||
|
||||
it('should return null if user not found', async () => {
|
||||
mockQueryBuilder.getOne.mockResolvedValueOnce(null);
|
||||
const result = await service.validateUser('unknown', 'password');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if password mismatch', async () => {
|
||||
bcrypt.compare.mockResolvedValueOnce(false);
|
||||
const result = await service.validateUser('testuser', 'wrongpassword');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should return access and refresh tokens', async () => {
|
||||
mockTokenRepo.create.mockReturnValue({ id: 1 });
|
||||
mockTokenRepo.save.mockResolvedValue({ id: 1 });
|
||||
|
||||
const result = await service.login(mockUser);
|
||||
|
||||
expect(result).toHaveProperty('access_token');
|
||||
expect(result).toHaveProperty('refresh_token');
|
||||
expect(mockTokenRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
(userService.findOneByUsername as jest.Mock).mockResolvedValue(null);
|
||||
(userService.create as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const dto = {
|
||||
username: 'newuser',
|
||||
password: 'password',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
};
|
||||
|
||||
const result = await service.register(dto);
|
||||
expect(result).toBeDefined();
|
||||
expect(userService.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should return new tokens if valid', async () => {
|
||||
const mockStoredToken = {
|
||||
tokenHash: 'somehash',
|
||||
isRevoked: false,
|
||||
expiresAt: new Date(Date.now() + 10000),
|
||||
};
|
||||
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
|
||||
(userService.findOne as jest.Mock).mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.refreshToken(1, 'valid_refresh_token');
|
||||
|
||||
expect(result.access_token).toBeDefined();
|
||||
expect(result.refresh_token).toBeDefined();
|
||||
// Should mark old token as revoked
|
||||
expect(mockTokenRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isRevoked: true })
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException if token revoked', async () => {
|
||||
const mockStoredToken = {
|
||||
tokenHash: 'somehash',
|
||||
isRevoked: true,
|
||||
expiresAt: new Date(Date.now() + 10000),
|
||||
};
|
||||
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
|
||||
|
||||
await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(
|
||||
UnauthorizedException
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// บันทึกการแก้ไข:
|
||||
// 1. แก้ไข Type Mismatch ใน signAsync
|
||||
// 2. แก้ไข validateUser ให้ดึง password_hash ออกมาด้วย (Fix HTTP 500: data and hash arguments required)
|
||||
// 3. [P2-2] Implement Refresh Token storage & rotation
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
@@ -12,14 +13,16 @@ import {
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { InjectRepository } from '@nestjs/typeorm'; // [NEW]
|
||||
import { Repository } from 'typeorm'; // [NEW]
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
import { User } from '../../modules/user/entities/user.entity.js'; // [NEW] ต้อง Import Entity เพื่อใช้ Repository
|
||||
import { RegisterDto } from './dto/register.dto.js';
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -28,31 +31,27 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
// [NEW] Inject Repository เพื่อใช้ QueryBuilder
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
// [P2-2] Inject RefreshToken Repository
|
||||
@InjectRepository(RefreshToken)
|
||||
private refreshTokenRepository: Repository<RefreshToken>
|
||||
) {}
|
||||
|
||||
// 1. ตรวจสอบ Username/Password
|
||||
async validateUser(username: string, pass: string): Promise<any> {
|
||||
console.log(`🔍 Checking login for: ${username}`); // [DEBUG]
|
||||
// [FIXED] ใช้ createQueryBuilder เพื่อ addSelect field 'password' ที่ถูกซ่อนไว้
|
||||
console.log(`🔍 Checking login for: ${username}`);
|
||||
const user = await this.usersRepository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.password') // สำคัญ! สั่งให้ดึง column password มาด้วย
|
||||
.addSelect('user.password')
|
||||
.where('user.username = :username', { username })
|
||||
.getOne();
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ User not found in database'); // [DEBUG]
|
||||
console.log('❌ User not found in database');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('✅ User found. Hash from DB:', user.password); // [DEBUG]
|
||||
|
||||
const isMatch = await bcrypt.compare(pass, user.password);
|
||||
console.log(`🔐 Password match result: ${isMatch}`); // [DEBUG]
|
||||
|
||||
// ตรวจสอบว่ามี user และมี password hash หรือไม่
|
||||
if (user && user.password && (await bcrypt.compare(pass, user.password))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -62,7 +61,7 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Login: สร้าง Access & Refresh Token
|
||||
// 2. Login: สร้าง Access & Refresh Token และบันทึกลง DB
|
||||
async login(user: any) {
|
||||
const payload = {
|
||||
username: user.username,
|
||||
@@ -70,20 +69,20 @@ export class AuthService {
|
||||
scope: 'Global',
|
||||
};
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
}),
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
}),
|
||||
]);
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
});
|
||||
|
||||
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
});
|
||||
|
||||
// [P2-2] Store Refresh Token in DB
|
||||
await this.storeRefreshToken(user.user_id, refreshToken);
|
||||
|
||||
return {
|
||||
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)
|
||||
async register(userDto: RegisterDto) {
|
||||
const existingUser = await this.userService.findOneByUsername(
|
||||
userDto.username,
|
||||
userDto.username
|
||||
);
|
||||
if (existingUser) {
|
||||
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) {
|
||||
// Hash incoming token to match with DB
|
||||
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex');
|
||||
|
||||
// Find token in DB
|
||||
const storedToken = await this.refreshTokenRepository.findOne({
|
||||
where: { tokenHash: hash },
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
if (storedToken.isRevoked) {
|
||||
// Possible token theft! Invalidate all user tokens family
|
||||
await this.revokeAllUserTokens(userId);
|
||||
throw new UnauthorizedException('Refresh token revoked - Security alert');
|
||||
}
|
||||
|
||||
if (storedToken.expiresAt < new Date()) {
|
||||
throw new UnauthorizedException('Refresh token expired');
|
||||
}
|
||||
|
||||
// Valid token -> Rotate it
|
||||
const user = await this.userService.findOne(userId);
|
||||
if (!user) throw new UnauthorizedException('User not found');
|
||||
|
||||
const payload = { username: user.username, sub: user.user_id };
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
// Generate NEW tokens
|
||||
const newAccessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
// ✅ Fix: Cast as any
|
||||
expiresIn: (this.configService.get<string>('JWT_EXPIRATION') ||
|
||||
'15m') as any,
|
||||
});
|
||||
|
||||
const newRefreshToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
expiresIn: (this.configService.get<string>('JWT_REFRESH_EXPIRATION') ||
|
||||
'7d') as any,
|
||||
});
|
||||
|
||||
// Revoke OLD token and point to NEW one
|
||||
const newHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(newRefreshToken)
|
||||
.digest('hex');
|
||||
|
||||
storedToken.isRevoked = true;
|
||||
storedToken.replacedByToken = newHash;
|
||||
await this.refreshTokenRepository.save(storedToken);
|
||||
|
||||
// Save NEW token
|
||||
await this.storeRefreshToken(userId, newRefreshToken);
|
||||
|
||||
return {
|
||||
access_token: accessToken,
|
||||
access_token: newAccessToken,
|
||||
refresh_token: newRefreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Logout: นำ Token เข้า Blacklist ใน Redis
|
||||
async logout(userId: number, accessToken: string) {
|
||||
// [P2-2] Helper: Revoke all tokens for a user (Security Measure)
|
||||
private async revokeAllUserTokens(userId: number) {
|
||||
await this.refreshTokenRepository.update(
|
||||
{ userId, isRevoked: false },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Logout: Revoke current refresh token & Blacklist Access Token
|
||||
async logout(userId: number, accessToken: string, refreshToken?: string) {
|
||||
// Blacklist Access Token
|
||||
try {
|
||||
const decoded = this.jwtService.decode(accessToken);
|
||||
if (decoded && decoded.exp) {
|
||||
@@ -139,13 +208,65 @@ export class AuthService {
|
||||
await this.cacheManager.set(
|
||||
`blacklist:token:${accessToken}`,
|
||||
true,
|
||||
ttl * 1000,
|
||||
ttl * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore decoding error
|
||||
}
|
||||
|
||||
// [P2-2] Revoke Refresh Token if provided
|
||||
if (refreshToken) {
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(refreshToken)
|
||||
.digest('hex');
|
||||
await this.refreshTokenRepository.update(
|
||||
{ tokenHash: hash },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
|
||||
return { message: 'Logged out successfully' };
|
||||
}
|
||||
|
||||
// [New] Get Active Sessions
|
||||
async getActiveSessions() {
|
||||
// Only return tokens that are NOT revoked and NOT expired
|
||||
const activeTokens = await this.refreshTokenRepository.find({
|
||||
where: {
|
||||
isRevoked: false,
|
||||
},
|
||||
relations: ['user'], // Ensure relations: ['user'] works if RefreshToken entity has relation
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
// Filter expired tokens in memory if query builder is complex, or rely on where clause if possible.
|
||||
// Filter expired tokens
|
||||
return activeTokens
|
||||
.filter((t) => new Date(t.expiresAt) > now)
|
||||
.map((t) => ({
|
||||
id: t.tokenId.toString(),
|
||||
userId: t.userId,
|
||||
user: {
|
||||
username: t.user?.username || 'Unknown',
|
||||
firstName: t.user?.firstName || '',
|
||||
lastName: t.user?.lastName || '',
|
||||
},
|
||||
deviceName: 'Unknown Device', // Not stored in DB
|
||||
ipAddress: 'Unknown IP', // Not stored in DB
|
||||
lastActive: t.createdAt.toISOString(), // Best approximation
|
||||
isCurrent: false, // Cannot determine isCurrent without current session context match
|
||||
}));
|
||||
}
|
||||
|
||||
// [New] Revoke Session by ID
|
||||
async revokeSession(sessionId: number) {
|
||||
return this.refreshTokenRepository.update(
|
||||
{ tokenId: sessionId },
|
||||
{ isRevoked: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Ability,
|
||||
AbilityBuilder,
|
||||
AbilityClass,
|
||||
ExtractSubjectType,
|
||||
InferSubjects,
|
||||
} from '@casl/ability';
|
||||
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
import { UserAssignment } from '../../../modules/user/entities/user-assignment.entity';
|
||||
|
||||
@@ -45,7 +39,7 @@ export class AbilityFactory {
|
||||
* - Level 4: Contract
|
||||
*/
|
||||
createForUser(user: User, context: ScopeContext): AppAbility {
|
||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
|
||||
const { can, build } = new AbilityBuilder<AppAbility>(
|
||||
Ability as AbilityClass<AppAbility>
|
||||
);
|
||||
|
||||
@@ -54,12 +48,13 @@ export class AbilityFactory {
|
||||
return build();
|
||||
}
|
||||
|
||||
// Iterate through user's role assignments
|
||||
// Iterate through user's role assignments
|
||||
user.assignments.forEach((assignment: UserAssignment) => {
|
||||
// Check if assignment matches the current context
|
||||
if (this.matchesScope(assignment, context)) {
|
||||
// Grant permissions from the role
|
||||
assignment.role.permissions.forEach((permission) => {
|
||||
assignment.role.permissions?.forEach((permission) => {
|
||||
const [action, subject] = this.parsePermission(
|
||||
permission.permissionName
|
||||
);
|
||||
@@ -70,8 +65,10 @@ export class AbilityFactory {
|
||||
|
||||
return build({
|
||||
// Detect subject type (for future use with objects)
|
||||
detectSubjectType: (item) =>
|
||||
item.constructor as ExtractSubjectType<Subjects>,
|
||||
detectSubjectType: (item: any) => {
|
||||
if (typeof item === 'string') return item;
|
||||
return item.constructor;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,17 +117,17 @@ export class AbilityFactory {
|
||||
* "project.view" → ["view", "project"]
|
||||
*/
|
||||
private parsePermission(permissionName: string): [string, string] {
|
||||
// Fallback for special permissions like "system.manage_all"
|
||||
if (permissionName === 'system.manage_all') {
|
||||
return ['manage', 'all'];
|
||||
}
|
||||
|
||||
const parts = permissionName.split('.');
|
||||
if (parts.length === 2) {
|
||||
const [subject, action] = parts;
|
||||
return [action, subject];
|
||||
}
|
||||
|
||||
// Fallback for special permissions like "system.manage_all"
|
||||
if (permissionName === 'system.manage_all') {
|
||||
return ['manage', 'all'];
|
||||
}
|
||||
|
||||
throw new Error(`Invalid permission format: ${permissionName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
description: 'Username (Email)',
|
||||
example: 'admin@np-dms.work',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@ApiProperty({ description: 'Password', example: 'password123' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password!: string;
|
||||
|
||||
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
|
||||
const hasPermission = requiredPermissions.every((permission) => {
|
||||
const [action, subject] = this.parsePermission(permission);
|
||||
return ability.can(action, subject);
|
||||
return ability.can(action as any, subject as any);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
|
||||
@@ -47,9 +47,9 @@ export class AuditLog {
|
||||
@Column({ name: 'user_agent', length: 255, nullable: true })
|
||||
userAgent?: string;
|
||||
|
||||
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
|
||||
// ✅ [Fix] ทั้งสอง Decorator ต้องระบุ name: 'created_at'
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
|
||||
@PrimaryColumn({ name: 'created_at' }) // Composite PK คู่กับ auditId
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import {
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
} from 'typeorm';
|
||||
import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
// @PrimaryGeneratedColumn()
|
||||
// id!: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
created_at!: Date;
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updated_at!: Date;
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
|
||||
deleted_at!: Date;
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../../modules/user/entities/user.entity.js';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
|
||||
@Entity('attachments')
|
||||
export class Attachment {
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileStorageController } from './file-storage.controller';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
|
||||
describe('FileStorageController', () => {
|
||||
let controller: FileStorageController;
|
||||
let mockFileStorageService: Partial<FileStorageService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFileStorageService = {
|
||||
upload: jest.fn(),
|
||||
download: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FileStorageController],
|
||||
providers: [
|
||||
{
|
||||
provide: FileStorageService,
|
||||
useValue: mockFileStorageService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<FileStorageController>(FileStorageController);
|
||||
@@ -15,4 +29,25 @@ describe('FileStorageController', () => {
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should upload a file successfully', async () => {
|
||||
const mockFile = {
|
||||
originalname: 'test.pdf',
|
||||
buffer: Buffer.from('test'),
|
||||
mimetype: 'application/pdf',
|
||||
size: 100,
|
||||
} as Express.Multer.File;
|
||||
|
||||
const mockResult = { attachment_id: 1, originalFilename: 'test.pdf' };
|
||||
(mockFileStorageService.upload as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const mockReq = { user: { userId: 1, username: 'testuser' } };
|
||||
const result = await controller.uploadFile(mockFile, mockReq as any);
|
||||
|
||||
expect(mockFileStorageService.upload).toHaveBeenCalledWith(mockFile, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
|
||||
// Interface เพื่อระบุ Type ของ Request ที่ผ่าน JwtAuthGuard มาแล้ว
|
||||
interface RequestWithUser {
|
||||
@@ -47,10 +47,10 @@ export class FileStorageController {
|
||||
/(pdf|msword|openxmlformats|zip|octet-stream|image|jpeg|png)/,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
@Request() req: RequestWithUser,
|
||||
@Request() req: RequestWithUser
|
||||
) {
|
||||
// ส่ง userId จาก Token ไปด้วย
|
||||
return this.fileStorageService.upload(file, req.user.userId);
|
||||
@@ -63,7 +63,7 @@ export class FileStorageController {
|
||||
@Get(':id/download')
|
||||
async downloadFile(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Res({ passthrough: true }) res: Response
|
||||
): Promise<StreamableFile> {
|
||||
const { stream, attachment } = await this.fileStorageService.download(id);
|
||||
|
||||
@@ -87,7 +87,7 @@ export class FileStorageController {
|
||||
@Delete(':id')
|
||||
async deleteFile(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Request() req: RequestWithUser,
|
||||
@Request() req: RequestWithUser
|
||||
) {
|
||||
// ส่ง userId ไปด้วยเพื่อตรวจสอบความเป็นเจ้าของ
|
||||
await this.fileStorageService.delete(id, req.user.userId);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { FileStorageController } from './file-storage.controller.js';
|
||||
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -1,18 +1,142 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as fs from 'fs-extra';
|
||||
import {
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
// Mock fs-extra
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('FileStorageService', () => {
|
||||
let service: FileStorageService;
|
||||
let attachmentRepo: Repository<Attachment>;
|
||||
|
||||
const mockAttachment = {
|
||||
id: 1,
|
||||
originalFilename: 'test.pdf',
|
||||
storedFilename: 'uuid.pdf',
|
||||
filePath: '/permanent/2024/12/uuid.pdf',
|
||||
fileSize: 1024,
|
||||
uploadedByUserId: 1,
|
||||
} as Attachment;
|
||||
|
||||
const mockFile = {
|
||||
originalname: 'test.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
size: 1024,
|
||||
buffer: Buffer.from('test-content'),
|
||||
} as Express.Multer.File;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FileStorageService],
|
||||
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();
|
||||
|
||||
service = module.get<FileStorageService>(FileStorageService);
|
||||
attachmentRepo = module.get(getRepositoryToken(Attachment));
|
||||
|
||||
jest.clearAllMocks();
|
||||
(fs.ensureDirSync as jest.Mock).mockReturnValue(true);
|
||||
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true);
|
||||
(fs.move as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.remove as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('upload', () => {
|
||||
it('should save file to temp and create DB record', async () => {
|
||||
const result = await service.upload(mockFile, 1);
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalled();
|
||||
expect(attachmentRepo.create).toHaveBeenCalled();
|
||||
expect(attachmentRepo.save).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if write fails', async () => {
|
||||
(fs.writeFile as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('Write error')
|
||||
);
|
||||
await expect(service.upload(mockFile, 1)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should move files to permanent storage', async () => {
|
||||
const tempIds = ['uuid-1'];
|
||||
const mockAttachments = [
|
||||
{
|
||||
...mockAttachment,
|
||||
isTemporary: true,
|
||||
tempId: 'uuid-1',
|
||||
filePath: '/temp/uuid.pdf',
|
||||
},
|
||||
];
|
||||
|
||||
(attachmentRepo.find as jest.Mock).mockResolvedValue(mockAttachments);
|
||||
|
||||
await service.commit(tempIds);
|
||||
|
||||
expect(fs.ensureDir).toHaveBeenCalled();
|
||||
expect(fs.move).toHaveBeenCalled();
|
||||
expect(attachmentRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show warning if file counts mismatch', async () => {
|
||||
(attachmentRepo.find as jest.Mock).mockResolvedValue([]);
|
||||
await expect(service.commit(['uuid-1'])).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete file if user owns it', async () => {
|
||||
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
|
||||
|
||||
await service.delete(1, 1);
|
||||
|
||||
expect(fs.remove).toHaveBeenCalled();
|
||||
expect(attachmentRepo.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if user does not own file', async () => {
|
||||
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
|
||||
await expect(service.delete(1, 999)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -5,25 +5,25 @@ import {
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js';
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
import { PERMISSIONS_KEY } from '../decorators/require-permission.decorator';
|
||||
import { UserService } from '../../modules/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class RbacGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private userService: UserService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
|
||||
const requiredPermission = this.reflector.getAllAndOverride<string>(
|
||||
PERMISSION_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()]
|
||||
);
|
||||
|
||||
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
|
||||
if (!requiredPermission) {
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -34,19 +34,20 @@ export class RbacGuard implements CanActivate {
|
||||
}
|
||||
|
||||
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
|
||||
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
|
||||
const userPermissions = await this.userService.getUserPermissions(
|
||||
user.userId,
|
||||
user.user_id // ✅ FIX: ใช้ user_id ตาม Entity field name
|
||||
);
|
||||
|
||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม?
|
||||
const hasPermission = userPermissions.some(
|
||||
(p) => p === requiredPermission || p === 'system.manage_all', // Superadmin ทะลุทุกสิทธิ์
|
||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? (User ต้องมีครบทุกสิทธิ์)
|
||||
const hasPermission = requiredPermissions.every((req) =>
|
||||
userPermissions.some(
|
||||
(p) => p === req || p === 'system.manage_all' // Superadmin ทะลุทุกสิทธิ์
|
||||
)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
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;
|
||||
message: string;
|
||||
data: T;
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -19,14 +20,29 @@ export class TransformInterceptor<T>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
next: CallHandler
|
||||
): Observable<Response<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
statusCode: context.switchToHttp().getResponse().statusCode,
|
||||
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
|
||||
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
|
||||
})),
|
||||
map((data: any) => {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
|
||||
// Handle Pagination Response (Standardize)
|
||||
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
|
||||
if (data && data.data && data.meta) {
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: data.message || 'Success',
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: data?.message || 'Success',
|
||||
data: data?.result || data,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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) {
|
||||
const repo = dataSource.getRepository(Organization);
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../modules/users/entities/user.entity';
|
||||
import { Role } from '../../modules/auth/entities/role.entity';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { Role, RoleScope } from '../../modules/user/entities/role.entity';
|
||||
import { UserAssignment } from '../../modules/user/entities/user-assignment.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export async function seedUsers(dataSource: DataSource) {
|
||||
const userRepo = dataSource.getRepository(User);
|
||||
const roleRepo = dataSource.getRepository(Role);
|
||||
const assignmentRepo = dataSource.getRepository(UserAssignment);
|
||||
|
||||
// Create Roles
|
||||
const rolesData = [
|
||||
{
|
||||
roleName: 'Superadmin',
|
||||
scope: RoleScope.GLOBAL,
|
||||
description:
|
||||
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',
|
||||
},
|
||||
{
|
||||
roleName: 'Org Admin',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Document Control',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Editor',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',
|
||||
},
|
||||
{
|
||||
roleName: 'Viewer',
|
||||
scope: RoleScope.PROJECT,
|
||||
description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',
|
||||
},
|
||||
{
|
||||
roleName: 'Project Manager',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',
|
||||
},
|
||||
{
|
||||
roleName: 'Contract Admin',
|
||||
scope: RoleScope.CONTRACT,
|
||||
description:
|
||||
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',
|
||||
},
|
||||
@@ -49,6 +58,7 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
for (const r of rolesData) {
|
||||
let role = await roleRepo.findOneBy({ roleName: r.roleName });
|
||||
if (!role) {
|
||||
// @ts-ignore
|
||||
role = await roleRepo.save(roleRepo.create(r));
|
||||
}
|
||||
roleMap.set(r.roleName, role);
|
||||
@@ -87,20 +97,30 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
];
|
||||
|
||||
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) {
|
||||
const exists = await userRepo.findOneBy({ username: u.username });
|
||||
if (!exists) {
|
||||
const user = userRepo.create({
|
||||
let user = await userRepo.findOneBy({ username: u.username });
|
||||
if (!user) {
|
||||
user = userRepo.create({
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
passwordHash,
|
||||
roles: [roleMap.get(u.roleName)],
|
||||
password, // Fixed: password instead of passwordHash
|
||||
});
|
||||
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');
|
||||
|
||||
// 🛡️ 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)
|
||||
app.enableCors({
|
||||
origin: true, // หรือ configService.get('CORS_ORIGIN')
|
||||
origin: configService.get<string>('CORS_ORIGIN') || true,
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true,
|
||||
});
|
||||
@@ -47,7 +60,7 @@ async function bootstrap() {
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true, // ช่วยแปลง Type ใน Query Params
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// ลงทะเบียน Global Interceptor และ Filter ที่เราสร้างไว้
|
||||
@@ -73,9 +86,9 @@ async function bootstrap() {
|
||||
|
||||
// 🚀 7. Start Server
|
||||
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(`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,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
@Entity('permissions')
|
||||
export class Permission {
|
||||
@PrimaryGeneratedColumn()
|
||||
export class Permission extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'permission_id' })
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'permission_code', length: 50, unique: true })
|
||||
permissionCode!: string;
|
||||
@Column({ name: 'permission_name', length: 100, unique: true })
|
||||
permissionName!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ name: 'resource', length: 50 })
|
||||
resource!: string;
|
||||
@Column({ name: 'module', length: 50, nullable: true })
|
||||
module?: string;
|
||||
|
||||
@Column({ name: 'action', length: 50 })
|
||||
action!: string;
|
||||
@Column({
|
||||
name: 'scope_level',
|
||||
type: 'enum',
|
||||
enum: ['GLOBAL', 'ORG', 'PROJECT'],
|
||||
nullable: true,
|
||||
})
|
||||
scopeLevel?: 'GLOBAL' | 'ORG' | 'PROJECT';
|
||||
|
||||
@Column({ name: 'is_active', default: true, type: 'tinyint' })
|
||||
isActive!: boolean;
|
||||
}
|
||||
|
||||
@Entity('roles')
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn()
|
||||
export class Role extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'role_id' })
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'role_name', length: 50, unique: true })
|
||||
@Column({ name: 'role_name', length: 100, unique: true })
|
||||
roleName!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['Global', 'Organization', 'Project', 'Contract'],
|
||||
default: 'Global',
|
||||
})
|
||||
scope!: 'Global' | 'Organization' | 'Project' | 'Contract';
|
||||
|
||||
@Column({ name: 'is_system', default: false })
|
||||
isSystem!: boolean;
|
||||
|
||||
@ManyToMany(() => Permission)
|
||||
@JoinTable({
|
||||
name: 'role_permissions',
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CirculationService {
|
||||
|
||||
try {
|
||||
// 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
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
@@ -51,7 +51,7 @@ export class CirculationService {
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
correspondenceId: createDto.correspondenceId,
|
||||
circulationNo: circulationNo,
|
||||
circulationNo: result.number,
|
||||
subject: createDto.subject,
|
||||
statusCode: 'OPEN',
|
||||
createdByUserId: user.user_id,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Circulation } from './circulation.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
|
||||
@Entity('circulation_routings')
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||
import { CirculationRouting } from './circulation-routing.entity';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { CreateContractDto } from './dto/create-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 { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@@ -38,11 +39,10 @@ export class ContractController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get All Contracts (Optional: filter by projectId)',
|
||||
summary: 'Get All Contracts (Search & Filter)',
|
||||
})
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
findAll(@Query('projectId') projectId?: number) {
|
||||
return this.contractService.findAll(projectId);
|
||||
findAll(@Query() query: SearchContractDto) {
|
||||
return this.contractService.findAll(query);
|
||||
}
|
||||
|
||||
@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,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { Repository, Like } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
|
||||
@@ -29,17 +29,53 @@ export class ContractService {
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async findAll(projectId?: number) {
|
||||
const query = this.contractRepo
|
||||
.createQueryBuilder('c')
|
||||
.leftJoinAndSelect('c.project', 'p')
|
||||
.orderBy('c.contractCode', 'ASC');
|
||||
async findAll(params?: any) {
|
||||
const { search, projectId, page = 1, limit = 100 } = params || {};
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
if (projectId) {
|
||||
query.where('c.projectId = :projectId', { projectId });
|
||||
const findOptions: any = {
|
||||
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) {
|
||||
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 { Contract } from './contract.entity.js';
|
||||
import { Organization } from './organization.entity.js';
|
||||
import { Contract } from './contract.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
|
||||
@Entity('contract_organizations')
|
||||
export class ContractOrganization {
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js';
|
||||
import { Project } from './project.entity.js';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
|
||||
@Entity('contracts')
|
||||
export class Contract extends BaseEntity {
|
||||
@@ -23,13 +23,14 @@ export class CorrespondenceWorkflowService {
|
||||
private readonly revisionRepo: Repository<CorrespondenceRevision>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private readonly statusRepo: Repository<CorrespondenceStatus>,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async submitWorkflow(
|
||||
correspondenceId: number,
|
||||
userId: number,
|
||||
note?: string,
|
||||
userRoles: string[], // [FIX] Added roles for DSL requirements check
|
||||
note?: string
|
||||
) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -44,7 +45,7 @@ export class CorrespondenceWorkflowService {
|
||||
|
||||
if (!revision) {
|
||||
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,
|
||||
'correspondence_revision',
|
||||
revision.id.toString(),
|
||||
context,
|
||||
context
|
||||
);
|
||||
|
||||
const transitionResult = await this.workflowEngine.processTransition(
|
||||
@@ -74,7 +75,7 @@ export class CorrespondenceWorkflowService {
|
||||
'SUBMIT',
|
||||
userId,
|
||||
note || 'Initial Submission',
|
||||
{},
|
||||
{ roles: userRoles } // [FIX] Pass roles for DSL requirements check
|
||||
);
|
||||
|
||||
await this.syncStatus(revision, transitionResult.nextState, queryRunner);
|
||||
@@ -97,14 +98,14 @@ export class CorrespondenceWorkflowService {
|
||||
async processAction(
|
||||
instanceId: string,
|
||||
userId: number,
|
||||
dto: WorkflowTransitionDto,
|
||||
dto: WorkflowTransitionDto
|
||||
) {
|
||||
const result = await this.workflowEngine.processTransition(
|
||||
instanceId,
|
||||
dto.action,
|
||||
userId,
|
||||
dto.comment,
|
||||
dto.payload,
|
||||
dto.payload
|
||||
);
|
||||
|
||||
// ✅ FIX: Method exists now
|
||||
@@ -125,7 +126,7 @@ export class CorrespondenceWorkflowService {
|
||||
private async syncStatus(
|
||||
revision: CorrespondenceRevision,
|
||||
workflowState: string,
|
||||
queryRunner?: any,
|
||||
queryRunner?: any
|
||||
) {
|
||||
const statusMap: Record<string, string> = {
|
||||
DRAFT: 'DRAFT',
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CorrespondenceController } from './correspondence.controller';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
|
||||
describe('CorrespondenceController', () => {
|
||||
let controller: CorrespondenceController;
|
||||
let mockCorrespondenceService: Partial<CorrespondenceService>;
|
||||
let mockWorkflowService: Partial<CorrespondenceWorkflowService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCorrespondenceService = {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
getReferences: jest.fn(),
|
||||
addReference: jest.fn(),
|
||||
removeReference: jest.fn(),
|
||||
};
|
||||
|
||||
mockWorkflowService = {
|
||||
submitWorkflow: jest.fn(),
|
||||
processAction: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CorrespondenceController],
|
||||
}).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);
|
||||
});
|
||||
@@ -15,4 +50,68 @@ describe('CorrespondenceController', () => {
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return correspondences', async () => {
|
||||
const mockResult = [{ id: 1 }];
|
||||
(mockCorrespondenceService.findAll as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const result = await controller.findAll({});
|
||||
|
||||
expect(mockCorrespondenceService.findAll).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a correspondence', async () => {
|
||||
const mockCorr = { id: 1, correspondenceNumber: 'TEST-001' };
|
||||
(mockCorrespondenceService.create as jest.Mock).mockResolvedValue(
|
||||
mockCorr
|
||||
);
|
||||
|
||||
const mockReq = { user: { user_id: 1 } };
|
||||
const createDto = {
|
||||
projectId: 1,
|
||||
typeId: 1,
|
||||
title: 'Test Subject',
|
||||
};
|
||||
|
||||
const result = await controller.create(
|
||||
createDto as Parameters<typeof controller.create>[0],
|
||||
mockReq as Parameters<typeof controller.create>[1]
|
||||
);
|
||||
|
||||
expect(mockCorrespondenceService.create).toHaveBeenCalledWith(
|
||||
createDto,
|
||||
mockReq.user
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
it('should submit a correspondence to workflow', async () => {
|
||||
const mockResult = { instanceId: 'inst-1', currentState: 'IN_REVIEW' };
|
||||
(mockWorkflowService.submitWorkflow as jest.Mock).mockResolvedValue(
|
||||
mockResult
|
||||
);
|
||||
|
||||
const mockReq = { user: { user_id: 1 } };
|
||||
const result = await controller.submit(
|
||||
1,
|
||||
{ note: 'Test note' },
|
||||
mockReq as Parameters<typeof controller.submit>[2]
|
||||
);
|
||||
|
||||
expect(mockWorkflowService.submitWorkflow).toHaveBeenCalledWith(
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
'Test note'
|
||||
);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,91 +5,209 @@ import {
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
Param, // <--- ✅ 1. เพิ่ม Param
|
||||
ParseIntPipe, // <--- ✅ 2. เพิ่ม ParseIntPipe
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
Delete,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { CorrespondenceService } from './correspondence.service.js';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto.js'; // <--- ✅ 3. เพิ่ม Import DTO นี้
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
// ... imports ...
|
||||
import { AddReferenceDto } from './dto/add-reference.dto.js';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
|
||||
import { Query, Delete } from '@nestjs/common'; // เพิ่ม Query, Delete
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
|
||||
@ApiTags('Correspondences')
|
||||
@Controller('correspondences')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@ApiBearerAuth()
|
||||
export class CorrespondenceController {
|
||||
constructor(private readonly correspondenceService: CorrespondenceService) {}
|
||||
constructor(
|
||||
private readonly correspondenceService: CorrespondenceService,
|
||||
private readonly workflowService: CorrespondenceWorkflowService
|
||||
) {}
|
||||
|
||||
@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(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@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()
|
||||
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
|
||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
||||
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
|
||||
return this.correspondenceService.create(createDto, req.user);
|
||||
@ApiOperation({ summary: 'Create new correspondence' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Correspondence created successfully.',
|
||||
type: CreateCorrespondenceDto,
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.create', 'correspondence')
|
||||
create(
|
||||
@Body() createDto: CreateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
return this.correspondenceService.create(
|
||||
createDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
}
|
||||
|
||||
@Post('preview-number')
|
||||
@ApiOperation({ summary: 'Preview next document number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return preview number and status.',
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
previewNumber(
|
||||
@Body() createDto: CreateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
return this.correspondenceService.previewDocumentNumber(
|
||||
createDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ ปรับปรุง findAll ให้รับ Query Params
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search correspondences' })
|
||||
@ApiResponse({ status: 200, description: 'Return list of correspondences.' })
|
||||
@RequirePermission('document.view')
|
||||
findAll(@Query() searchDto: SearchCorrespondenceDto) {
|
||||
return this.correspondenceService.findAll(searchDto);
|
||||
}
|
||||
|
||||
// ✅ เพิ่ม Endpoint นี้ครับ
|
||||
@Post(':id/submit')
|
||||
@RequirePermission('correspondence.create') // หรือจะสร้าง Permission ใหม่ 'workflow.submit' ก็ได้
|
||||
@Audit('correspondence.create', 'correspondence') // ✅ แปะตรงนี้
|
||||
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Correspondence submitted successfully.',
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.submit', 'correspondence')
|
||||
submit(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@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,
|
||||
submitDto.templateId,
|
||||
req.user,
|
||||
req.user.user_id,
|
||||
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')
|
||||
@ApiOperation({ summary: 'Get referenced documents' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return list of referenced documents.',
|
||||
})
|
||||
@RequirePermission('document.view')
|
||||
getReferences(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.correspondenceService.getReferences(id);
|
||||
}
|
||||
|
||||
@Post(':id/references')
|
||||
@RequirePermission('document.edit') // ต้องมีสิทธิ์แก้ไขถึงจะเพิ่ม Ref ได้
|
||||
@ApiOperation({ summary: 'Add reference to another document' })
|
||||
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
addReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: AddReferenceDto,
|
||||
@Body() dto: AddReferenceDto
|
||||
) {
|
||||
return this.correspondenceService.addReference(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/references/:targetId')
|
||||
@ApiOperation({ summary: 'Remove reference' })
|
||||
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
removeReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('targetId', ParseIntPipe) targetId: number,
|
||||
@Param('targetId', ParseIntPipe) targetId: number
|
||||
) {
|
||||
return this.correspondenceService.removeReference(id, targetId);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CorrespondenceController } from './correspondence.controller.js';
|
||||
import { CorrespondenceService } from './correspondence.service.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity.js';
|
||||
import { Correspondence } from './entities/correspondence.entity.js';
|
||||
// Import Entities ใหม่
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity.js';
|
||||
import { RoutingTemplateStep } from './entities/routing-template-step.entity.js';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity.js';
|
||||
import { CorrespondenceController } from './correspondence.controller';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service';
|
||||
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module.js'; // ต้องใช้ตอน Create
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
||||
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
|
||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
||||
// Controllers & Services
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service'; // Register Service นี้
|
||||
// Entities
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
|
||||
// Dependent Modules
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
/**
|
||||
* CorrespondenceModule
|
||||
*
|
||||
* NOTE: RoutingTemplate and RoutingTemplateStep have been deprecated.
|
||||
* All workflow operations now use the Unified Workflow Engine.
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
@@ -27,19 +33,18 @@ import { CorrespondenceWorkflowService } from './correspondence-workflow.service
|
||||
CorrespondenceRevision,
|
||||
CorrespondenceType,
|
||||
CorrespondenceStatus,
|
||||
RoutingTemplate, // <--- ลงทะเบียน
|
||||
RoutingTemplateStep, // <--- ลงทะเบียน
|
||||
CorrespondenceRouting, // <--- ลงทะเบียน
|
||||
CorrespondenceReference, // <--- ลงทะเบียน
|
||||
CorrespondenceReference,
|
||||
CorrespondenceRecipient,
|
||||
Organization,
|
||||
]),
|
||||
DocumentNumberingModule, // Import เพื่อขอเลขที่เอกสาร
|
||||
JsonSchemaModule, // Import เพื่อ Validate JSON
|
||||
UserModule, // <--- 2. ใส่ UserModule ใน imports เพื่อให้ RbacGuard ทำงานได้
|
||||
WorkflowEngineModule, // <--- Import WorkflowEngine
|
||||
SearchModule, // ✅ 2. ใส่ SearchModule ที่นี่
|
||||
DocumentNumberingModule,
|
||||
JsonSchemaModule,
|
||||
UserModule,
|
||||
WorkflowEngineModule,
|
||||
SearchModule,
|
||||
],
|
||||
controllers: [CorrespondenceController],
|
||||
providers: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||
exports: [CorrespondenceService],
|
||||
exports: [CorrespondenceService, CorrespondenceWorkflowService],
|
||||
})
|
||||
export class CorrespondenceModule {}
|
||||
|
||||
@@ -1,18 +1,270 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { CorrespondenceService } from './correspondence.service';
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
describe('CorrespondenceService', () => {
|
||||
let service: CorrespondenceService;
|
||||
let numberingService: DocumentNumberingService;
|
||||
let correspondenceRepo: any;
|
||||
let revisionRepo: any;
|
||||
let dataSource: any;
|
||||
|
||||
const createMockRepository = () => ({
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue(null),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
})),
|
||||
});
|
||||
|
||||
const mockDataSource = {
|
||||
createQueryRunner: jest.fn(() => ({
|
||||
connect: jest.fn(),
|
||||
startTransaction: jest.fn(),
|
||||
commitTransaction: jest.fn(),
|
||||
rollbackTransaction: jest.fn(),
|
||||
release: jest.fn(),
|
||||
manager: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
})),
|
||||
getRepository: jest.fn(() => createMockRepository()),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [CorrespondenceService],
|
||||
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();
|
||||
|
||||
service = module.get<CorrespondenceService>(CorrespondenceService);
|
||||
numberingService = module.get<DocumentNumberingService>(
|
||||
DocumentNumberingService
|
||||
);
|
||||
correspondenceRepo = module.get(getRepositoryToken(Correspondence));
|
||||
revisionRepo = module.get(getRepositoryToken(CorrespondenceRevision));
|
||||
dataSource = module.get(DataSource);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should NOT regenerate number if critical fields unchanged', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
}; // Status 5 = Draft handled by logic?
|
||||
// Mock status repo to return DRAFT
|
||||
// But strict logic: revision.statusId check
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
const mockStatus = { id: 5, statusCode: 'DRAFT' };
|
||||
// Need to set statusRepo mock behavior... simplified here for brevity or assume defaults
|
||||
// Injecting internal access to statusRepo is hard without `module.get` if I didn't save it.
|
||||
// Let's assume it passes check for now.
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
// Update DTO with same values
|
||||
const updateDto = {
|
||||
projectId: 1,
|
||||
disciplineId: 3,
|
||||
// recipients missing -> imply no change
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
// Check that updateNumberForDraft was NOT called
|
||||
expect(numberingService.updateNumberForDraft).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should regenerate number if Project ID changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1, // Old Project
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
const updateDto = {
|
||||
projectId: 2, // New Project -> Change!
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
it('should regenerate number if Document Type changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2, // Old Type
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
|
||||
const updateDto = {
|
||||
typeId: 999, // New Type
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should regenerate number if Recipient Organization changes', async () => {
|
||||
const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;
|
||||
const mockRevision = {
|
||||
id: 100,
|
||||
correspondenceId: 1,
|
||||
isCurrent: true,
|
||||
statusId: 5,
|
||||
};
|
||||
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);
|
||||
|
||||
const mockCorr = {
|
||||
id: 1,
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 2,
|
||||
disciplineId: 3,
|
||||
originatorId: 10,
|
||||
correspondenceNumber: 'OLD-NUM',
|
||||
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], // Old Recipient 99
|
||||
};
|
||||
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);
|
||||
jest
|
||||
.spyOn(service['orgRepo'], 'findOne')
|
||||
.mockResolvedValue({ id: 88, organizationCode: 'NEW-ORG' } as any);
|
||||
|
||||
const updateDto = {
|
||||
recipients: [{ type: 'TO', organizationId: 88 }], // New Recipient 88
|
||||
};
|
||||
|
||||
await service.update(1, updateDto as any, mockUser);
|
||||
|
||||
expect(numberingService.updateNumberForDraft).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,26 +9,24 @@ import {
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
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 { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
@@ -37,6 +35,12 @@ import { WorkflowEngineService } from '../workflow-engine/workflow-engine.servic
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
/**
|
||||
* CorrespondenceService - Document management (CRUD)
|
||||
*
|
||||
* NOTE: Workflow operations (submit, processAction) have been moved to
|
||||
* CorrespondenceWorkflowService which uses the Unified Workflow Engine.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CorrespondenceService {
|
||||
private readonly logger = new Logger(CorrespondenceService.name);
|
||||
@@ -50,12 +54,10 @@ export class CorrespondenceService {
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
@InjectRepository(RoutingTemplate)
|
||||
private templateRepo: Repository<RoutingTemplate>,
|
||||
@InjectRepository(CorrespondenceRouting)
|
||||
private routingRepo: Repository<CorrespondenceRouting>,
|
||||
@InjectRepository(CorrespondenceReference)
|
||||
private referenceRepo: Repository<CorrespondenceReference>,
|
||||
@InjectRepository(Organization)
|
||||
private orgRepo: Repository<Organization>,
|
||||
|
||||
private numberingService: DocumentNumberingService,
|
||||
private jsonSchemaService: JsonSchemaService,
|
||||
@@ -111,9 +113,9 @@ export class CorrespondenceService {
|
||||
if (createDto.details) {
|
||||
try {
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
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 {
|
||||
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({
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
typeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
disciplineId: createDto.disciplineId,
|
||||
subTypeId: createDto.subTypeId,
|
||||
recipientOrganizationId, // [v1.5.1] Pass recipient for document number format
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: orgCode,
|
||||
RECIPIENT_CODE: recipientCode,
|
||||
REC_CODE: recipientCode,
|
||||
},
|
||||
});
|
||||
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId, // บันทึก Discipline ลง DB
|
||||
disciplineId: createDto.disciplineId,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
isInternal: createDto.isInternal || false,
|
||||
@@ -156,16 +172,32 @@ export class CorrespondenceService {
|
||||
revisionLabel: 'A',
|
||||
isCurrent: true,
|
||||
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,
|
||||
details: createDto.details,
|
||||
createdBy: user.user_id,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// Save Recipients
|
||||
if (createDto.recipients && createDto.recipients.length > 0) {
|
||||
const recipients = createDto.recipients.map((r) =>
|
||||
queryRunner.manager.create(CorrespondenceRecipient, {
|
||||
correspondenceId: savedCorr.id,
|
||||
recipientOrganizationId: r.organizationId,
|
||||
recipientType: r.type,
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(recipients);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// [NEW V1.5.1] Start Workflow Instance (After Commit)
|
||||
// Start Workflow Instance (non-blocking)
|
||||
try {
|
||||
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
||||
await this.workflowEngine.createInstance(
|
||||
@@ -183,14 +215,13 @@ export class CorrespondenceService {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||
);
|
||||
// Non-blocking: Document is created, but workflow might not be active.
|
||||
}
|
||||
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'correspondence',
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
title: createDto.subject,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
@@ -212,17 +243,35 @@ export class CorrespondenceService {
|
||||
}
|
||||
}
|
||||
|
||||
// ... (method อื่นๆ คงเดิม)
|
||||
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
|
||||
.createQueryBuilder('corr')
|
||||
.leftJoinAndSelect('corr.revisions', 'rev')
|
||||
// Change: Query from Revision Repo
|
||||
const query = this.revisionRepo
|
||||
.createQueryBuilder('rev')
|
||||
.leftJoinAndSelect('rev.correspondence', 'corr')
|
||||
.leftJoinAndSelect('corr.type', 'type')
|
||||
.leftJoinAndSelect('corr.project', 'project')
|
||||
.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) {
|
||||
query.andWhere('corr.projectId = :projectId', { projectId });
|
||||
@@ -238,14 +287,25 @@ export class CorrespondenceService {
|
||||
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :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) {
|
||||
@@ -257,6 +317,8 @@ export class CorrespondenceService {
|
||||
'type',
|
||||
'project',
|
||||
'originator',
|
||||
'recipients',
|
||||
'recipients.recipientOrganization', // [v1.5.1] Fixed relation name
|
||||
],
|
||||
});
|
||||
|
||||
@@ -266,182 +328,6 @@ export class CorrespondenceService {
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
async submit(correspondenceId: number, templateId: number, user: User) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
}
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision) {
|
||||
throw new NotFoundException('Current revision not found');
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: templateId },
|
||||
relations: ['steps'],
|
||||
order: { steps: { sequence: 'ASC' } },
|
||||
});
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined'
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const firstStep = template.steps[0];
|
||||
|
||||
const routing = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: 1,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: firstStep.toOrganizationId,
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
});
|
||||
await queryRunner.manager.save(routing);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return routing;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User
|
||||
) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence)
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
const currentRouting = await this.routingRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: currentRevision.id,
|
||||
status: 'SENT',
|
||||
},
|
||||
order: { sequence: 'DESC' },
|
||||
relations: ['toOrganization'],
|
||||
});
|
||||
|
||||
if (!currentRouting) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found for this document'
|
||||
);
|
||||
}
|
||||
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId'
|
||||
);
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: currentRouting.templateId },
|
||||
relations: ['steps'],
|
||||
});
|
||||
|
||||
if (!template || !template.steps) {
|
||||
throw new InternalServerErrorException('Template definition not found');
|
||||
}
|
||||
|
||||
const totalSteps = template.steps.length;
|
||||
const currentSeq = currentRouting.sequence;
|
||||
|
||||
const result = this.workflowEngine.processAction(
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
this.logger.warn(
|
||||
`Next step ${result.nextStepSequence} not found in template`
|
||||
);
|
||||
} else {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
CorrespondenceRouting,
|
||||
{
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: result.nextStepSequence,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: nextStepConfig.toOrganizationId,
|
||||
stepPurpose: nextStepConfig.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
}
|
||||
);
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: 'Action processed successfully', result };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async addReference(id: number, dto: AddReferenceDto) {
|
||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||
const target = await this.correspondenceRepo.findOne({
|
||||
@@ -499,4 +385,235 @@ export class CorrespondenceService {
|
||||
|
||||
return { outgoing, incoming };
|
||||
}
|
||||
|
||||
async update(id: number, updateDto: UpdateCorrespondenceDto, user: User) {
|
||||
// 1. Find Current Revision
|
||||
const revision = await this.revisionRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: id,
|
||||
isCurrent: true,
|
||||
},
|
||||
relations: ['correspondence'],
|
||||
});
|
||||
|
||||
if (!revision) {
|
||||
throw new NotFoundException(
|
||||
`Current revision for correspondence ${id} not found`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check Permission
|
||||
if (revision.statusId) {
|
||||
const status = await this.statusRepo.findOne({
|
||||
where: { id: revision.statusId },
|
||||
});
|
||||
if (status && status.statusCode !== 'DRAFT') {
|
||||
throw new BadRequestException('Only DRAFT documents can be updated');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update Correspondence Entity if needed
|
||||
const correspondenceUpdate: DeepPartial<Correspondence> = {};
|
||||
if (updateDto.disciplineId)
|
||||
correspondenceUpdate.disciplineId = updateDto.disciplineId;
|
||||
if (updateDto.projectId)
|
||||
correspondenceUpdate.projectId = updateDto.projectId;
|
||||
if (updateDto.originatorId)
|
||||
correspondenceUpdate.originatorId = updateDto.originatorId;
|
||||
|
||||
if (Object.keys(correspondenceUpdate).length > 0) {
|
||||
await this.correspondenceRepo.update(id, correspondenceUpdate);
|
||||
}
|
||||
|
||||
// 4. Update Revision Entity
|
||||
const revisionUpdate: DeepPartial<CorrespondenceRevision> = {};
|
||||
if (updateDto.subject) revisionUpdate.subject = updateDto.subject;
|
||||
if (updateDto.body) revisionUpdate.body = updateDto.body;
|
||||
if (updateDto.remarks) revisionUpdate.remarks = updateDto.remarks;
|
||||
// Format Date correctly if string
|
||||
if (updateDto.dueDate) revisionUpdate.dueDate = new Date(updateDto.dueDate);
|
||||
if (updateDto.description)
|
||||
revisionUpdate.description = updateDto.description;
|
||||
if (updateDto.details) revisionUpdate.details = updateDto.details;
|
||||
|
||||
if (Object.keys(revisionUpdate).length > 0) {
|
||||
await this.revisionRepo.update(revision.id, revisionUpdate);
|
||||
}
|
||||
|
||||
// 5. Update Recipients if provided
|
||||
if (updateDto.recipients) {
|
||||
const recipientRepo = this.dataSource.getRepository(
|
||||
CorrespondenceRecipient
|
||||
);
|
||||
await recipientRepo.delete({ correspondenceId: id });
|
||||
|
||||
const newRecipients = updateDto.recipients.map((r) =>
|
||||
recipientRepo.create({
|
||||
correspondenceId: id,
|
||||
recipientOrganizationId: r.organizationId,
|
||||
recipientType: r.type,
|
||||
})
|
||||
);
|
||||
await recipientRepo.save(newRecipients);
|
||||
}
|
||||
|
||||
// 6. Regenerate Document Number if structural fields changed (Recipient, Discipline, Type, Project)
|
||||
// AND it is a DRAFT.
|
||||
|
||||
// Fetch fresh data for context and comparison
|
||||
const currentCorr = await this.correspondenceRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['type', 'recipients', 'recipients.recipientOrganization'],
|
||||
});
|
||||
|
||||
if (currentCorr) {
|
||||
const currentToRecipient = currentCorr.recipients?.find(
|
||||
(r) => r.recipientType === 'TO'
|
||||
);
|
||||
const currentRecipientId = currentToRecipient?.recipientOrganizationId;
|
||||
|
||||
// Check for ACTUAL value changes
|
||||
const isProjectChanged =
|
||||
updateDto.projectId !== undefined &&
|
||||
updateDto.projectId !== currentCorr.projectId;
|
||||
const isOriginatorChanged =
|
||||
updateDto.originatorId !== undefined &&
|
||||
updateDto.originatorId !== currentCorr.originatorId;
|
||||
const isDisciplineChanged =
|
||||
updateDto.disciplineId !== undefined &&
|
||||
updateDto.disciplineId !== currentCorr.disciplineId;
|
||||
const isTypeChanged =
|
||||
updateDto.typeId !== undefined &&
|
||||
updateDto.typeId !== currentCorr.correspondenceTypeId;
|
||||
|
||||
let isRecipientChanged = false;
|
||||
let newRecipientId: number | undefined;
|
||||
|
||||
if (updateDto.recipients) {
|
||||
// Safe check for 'type' or 'recipientType' (mismatch safeguard)
|
||||
const newToRecipient = updateDto.recipients.find(
|
||||
(r: any) => r.type === 'TO' || r.recipientType === 'TO'
|
||||
);
|
||||
newRecipientId = newToRecipient?.organizationId;
|
||||
|
||||
if (newRecipientId !== currentRecipientId) {
|
||||
isRecipientChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isProjectChanged ||
|
||||
isDisciplineChanged ||
|
||||
isTypeChanged ||
|
||||
isRecipientChanged ||
|
||||
isOriginatorChanged
|
||||
) {
|
||||
const targetRecipientId = isRecipientChanged
|
||||
? newRecipientId
|
||||
: currentRecipientId;
|
||||
|
||||
// Resolve Recipient Code for the NEW context
|
||||
let recipientCode = '';
|
||||
if (targetRecipientId) {
|
||||
const recOrg = await this.orgRepo.findOne({
|
||||
where: { id: targetRecipientId },
|
||||
});
|
||||
if (recOrg) recipientCode = recOrg.organizationCode;
|
||||
}
|
||||
|
||||
const orgCode = 'ORG'; // Placeholder - should be fetched from Originator if needed in future
|
||||
|
||||
// Prepare Contexts
|
||||
const oldCtx = {
|
||||
projectId: currentCorr.projectId,
|
||||
originatorId: currentCorr.originatorId ?? 0,
|
||||
typeId: currentCorr.correspondenceTypeId,
|
||||
disciplineId: currentCorr.disciplineId,
|
||||
recipientOrganizationId: currentRecipientId,
|
||||
year: new Date().getFullYear(),
|
||||
};
|
||||
|
||||
const newCtx = {
|
||||
projectId: updateDto.projectId ?? currentCorr.projectId,
|
||||
originatorId: 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.previewNextNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorId: 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 { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AddReferenceDto {
|
||||
@ApiProperty({
|
||||
description: 'Target Correspondence ID to reference',
|
||||
example: 20,
|
||||
})
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
targetId!: number;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// File: src/modules/correspondence/dto/create-correspondence.dto.ts
|
||||
import {
|
||||
IsInt,
|
||||
IsString,
|
||||
@@ -6,43 +5,99 @@ import {
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsObject,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateCorrespondenceDto {
|
||||
@ApiProperty({ description: 'Project ID', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ description: 'Document Type ID', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
typeId!: number; // ID ของประเภทเอกสาร (เช่น RFA, LETTER)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Discipline ID', example: 2 })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
disciplineId?: number; // [Req 6B] สาขางาน (เช่น GEN, STR)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sub Type ID', example: 3 })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
subTypeId?: number; // [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA)
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Correspondence Subject',
|
||||
example: 'Monthly Progress Report',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
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()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Additional details (JSON)',
|
||||
example: { key: 'value' },
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is internal document?', default: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isInternal?: boolean;
|
||||
|
||||
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
|
||||
@ApiPropertyOptional({
|
||||
description: 'Originator Organization ID (for impersonation)',
|
||||
example: 1,
|
||||
})
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
originatorId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Recipients',
|
||||
example: [{ organizationId: 1, type: 'TO' }],
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
|
||||
}
|
||||
@@ -1,24 +1,52 @@
|
||||
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 {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search term (Title or Document Number)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string; // ค้นหาจาก Title หรือ Number
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Document Type ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
typeId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Project ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
projectId?: number;
|
||||
|
||||
// status อาจจะซับซ้อนหน่อยเพราะอยู่ที่ Revision แต่ใส่ไว้ก่อน
|
||||
@ApiPropertyOptional({ description: 'Filter by Status ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
statusId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Revision Filter: CURRENT (default), ALL, OLD',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
revisionStatus?: 'CURRENT' | 'ALL' | 'OLD';
|
||||
|
||||
@ApiPropertyOptional({ description: 'Page number (default 1)', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Items per page (default 10)',
|
||||
default: 10,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
@@ -1,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 {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
templateId!: number;
|
||||
@ApiPropertyOptional({
|
||||
description: 'Optional note for the submission',
|
||||
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 { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
|
||||
import { IsEnum, IsString, IsOptional, IsUUID, IsInt } from 'class-validator';
|
||||
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 {
|
||||
@IsEnum(WorkflowAction)
|
||||
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
|
||||
@ApiPropertyOptional({
|
||||
description: 'Workflow Instance ID (UUID) - for Unified Workflow Engine',
|
||||
example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
instanceId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Workflow Action',
|
||||
enum: ['APPROVE', 'REJECT', 'RETURN', 'CANCEL', 'ACKNOWLEDGE'],
|
||||
})
|
||||
@IsEnum(WorkflowAction)
|
||||
action!: WorkflowAction;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Review comments',
|
||||
example: 'Approved with note...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use 'comment' instead
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: 'Review comments (deprecated, use comment)',
|
||||
example: 'Approved with note...',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comments?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sequence to return to (only for RETURN action in legacy RFA)',
|
||||
example: 1,
|
||||
})
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
returnToSequence?: number; // ใช้กรณี 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 { Correspondence } from './correspondence.entity.js';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
|
||||
@Entity('correspondence_references')
|
||||
export class CorrespondenceReference {
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity.js';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
|
||||
@Entity('correspondence_revisions')
|
||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||
@@ -35,15 +35,24 @@ export class CorrespondenceRevision {
|
||||
@Column({ name: 'correspondence_status_id' })
|
||||
statusId!: number;
|
||||
|
||||
@Column({ length: 255 })
|
||||
title!: string;
|
||||
@Column({ length: 500 })
|
||||
subject!: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
body?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
remarks?: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type
|
||||
|
||||
@Column({ name: 'schema_version', default: 1 })
|
||||
schemaVersion!: number;
|
||||
|
||||
// ✅ [New] Virtual Column: ดึง Project ID จาก JSON details
|
||||
@Column({
|
||||
name: 'v_ref_project_id',
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity.js';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { RoutingTemplate } from './routing-template.entity';
|
||||
|
||||
@Entity('correspondence_routings')
|
||||
export class CorrespondenceRouting {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} 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 ตามจริง
|
||||
|
||||
@Entity('correspondence_sub_types')
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { CorrespondenceType } from './correspondence-type.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js'; // เดี๋ยวสร้าง
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from './correspondence-type.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Discipline } from '../../master/entities/discipline.entity';
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
@@ -68,9 +70,9 @@ export class Correspondence {
|
||||
creator?: User;
|
||||
|
||||
// [New V1.5.1]
|
||||
@ManyToOne('Discipline')
|
||||
@ManyToOne(() => Discipline)
|
||||
@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
|
||||
@OneToMany(
|
||||
@@ -78,4 +80,11 @@ export class Correspondence {
|
||||
(revision) => revision.correspondence
|
||||
)
|
||||
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,
|
||||
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';
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* @deprecated This entity is deprecated and will be removed in future versions.
|
||||
* Use WorkflowDefinition from the Unified Workflow Engine instead.
|
||||
*
|
||||
* This entity is kept for backward compatibility and historical data.
|
||||
* Relations have been removed to prevent TypeORM errors.
|
||||
*/
|
||||
@Entity('correspondence_routing_template_steps')
|
||||
export class RoutingTemplateStep {
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -24,27 +21,12 @@ export class RoutingTemplateStep {
|
||||
@Column({ name: 'to_organization_id' })
|
||||
toOrganizationId!: number;
|
||||
|
||||
@Column({ name: 'role_id', nullable: true })
|
||||
roleId?: number;
|
||||
|
||||
@Column({ name: 'step_purpose', default: 'FOR_REVIEW' })
|
||||
@Column({ name: 'step_purpose', length: 50, default: 'FOR_REVIEW' })
|
||||
stepPurpose!: string;
|
||||
|
||||
@Column({ name: 'expected_days', nullable: true })
|
||||
expectedDays?: number;
|
||||
@Column({ name: 'expected_days', default: 7 })
|
||||
expectedDays!: number;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => RoutingTemplate, (template) => template.steps, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template?: RoutingTemplate;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'to_organization_id' })
|
||||
toOrganization?: Organization;
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
@JoinColumn({ name: 'role_id' })
|
||||
role?: Role;
|
||||
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||
// template?: RoutingTemplate;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js'; // ถ้าไม่ได้ใช้ BaseEntity ก็ลบออกแล้วใส่ createdAt เอง
|
||||
import { RoutingTemplateStep } from './routing-template-step.entity.js'; // เดี๋ยวสร้าง
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* @deprecated This entity is deprecated and will be removed in future versions.
|
||||
* Use WorkflowDefinition from the Unified Workflow Engine instead.
|
||||
*
|
||||
* This entity is kept for backward compatibility and historical data.
|
||||
* The relation to RoutingTemplateStep has been removed to prevent TypeORM errors.
|
||||
*/
|
||||
@Entity('correspondence_routing_templates')
|
||||
export class RoutingTemplate {
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -14,14 +19,14 @@ export class RoutingTemplate {
|
||||
description?: string;
|
||||
|
||||
@Column({ name: 'project_id', nullable: true })
|
||||
projectId?: number; // NULL = แม่แบบทั่วไป
|
||||
projectId?: number;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ type: 'json', nullable: true, name: 'workflow_config' })
|
||||
workflowConfig?: any;
|
||||
workflowConfig?: Record<string, unknown>;
|
||||
|
||||
@OneToMany(() => RoutingTemplateStep, (step) => step.template)
|
||||
steps?: RoutingTemplateStep[];
|
||||
// @deprecated - Relation removed, use WorkflowDefinition instead
|
||||
// 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';
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
|
||||
@ApiTags('Admin / Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin/document-numbering')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class DocumentNumberingAdminController {
|
||||
constructor(private readonly service: DocumentNumberingService) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Template Management
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('templates')
|
||||
@ApiOperation({ summary: 'Get all document numbering templates' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async getTemplates(@Query('projectId') projectId?: number) {
|
||||
if (projectId) {
|
||||
return this.service.getTemplatesByProject(projectId);
|
||||
}
|
||||
return this.service.getTemplates();
|
||||
}
|
||||
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async saveTemplate(@Body() dto: any) {
|
||||
return this.service.saveTemplate(dto);
|
||||
}
|
||||
|
||||
@Delete('templates/:id')
|
||||
@ApiOperation({ summary: 'Delete a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
|
||||
await this.service.deleteTemplate(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Metrics & Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('metrics')
|
||||
@ApiOperation({ summary: 'Get numbering usage metrics and logs' })
|
||||
@RequirePermission('system.view_logs')
|
||||
async getMetrics() {
|
||||
const audit = await this.service.getAuditLogs(50);
|
||||
const errors = await this.service.getErrorLogs(50);
|
||||
return { audit, errors };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Admin Operations
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('manual-override')
|
||||
@ApiOperation({
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
@RequirePermission('system.manage_settings')
|
||||
async manualOverride(@Body() dto: any) {
|
||||
return this.service.manualOverride(dto);
|
||||
}
|
||||
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async voidAndReplace(@Body() dto: any) {
|
||||
return this.service.voidAndReplace(dto);
|
||||
}
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async cancelNumber(@Body() dto: any) {
|
||||
return this.service.cancelNumber(dto);
|
||||
}
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async bulkImport(@Body() items: any[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
import { PreviewNumberDto } from './dto/preview-number.dto';
|
||||
|
||||
@ApiTags('Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@Controller('document-numbering')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class DocumentNumberingController {
|
||||
constructor(private readonly numberingService: DocumentNumberingService) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('logs/audit')
|
||||
@ApiOperation({ summary: 'Get document generation audit logs' })
|
||||
@ApiResponse({ status: 200, description: 'List of audit logs' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@RequirePermission('system.view_logs')
|
||||
getAuditLogs(@Query('limit') limit?: number) {
|
||||
return this.numberingService.getAuditLogs(limit ? Number(limit) : 100);
|
||||
}
|
||||
|
||||
@Get('logs/errors')
|
||||
@ApiOperation({ summary: 'Get document generation error logs' })
|
||||
@ApiResponse({ status: 200, description: 'List of error logs' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@RequirePermission('system.view_logs')
|
||||
getErrorLogs(@Query('limit') limit?: number) {
|
||||
return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Sequences / Counters
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('sequences')
|
||||
@ApiOperation({ summary: 'Get all number sequences/counters' })
|
||||
@ApiResponse({ status: 200, description: 'List of counter sequences' })
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
@RequirePermission('correspondence.read')
|
||||
getSequences(@Query('projectId') projectId?: number) {
|
||||
return this.numberingService.getSequences(
|
||||
projectId ? Number(projectId) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@Patch('counters/:id')
|
||||
@ApiOperation({ summary: 'Update counter sequence value (Admin only)' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async updateCounter(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body('sequence') sequence: number
|
||||
) {
|
||||
return this.numberingService.setCounterValue(id, sequence);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Preview / Test
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('preview')
|
||||
@ApiOperation({ summary: 'Preview what a document number would look like' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Preview result without incrementing counter',
|
||||
})
|
||||
@RequirePermission('correspondence.read')
|
||||
async previewNumber(@Body() dto: PreviewNumberDto) {
|
||||
return this.numberingService.previewNumber(dto);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user