Version: 1.0.0
Base URL: http://localhost:5000/api
Date: January 24, 2026
- System Overview
- Complete API Endpoints Reference
- Backend Rules & Guarantees
- Data Models
- Security & Authentication
- Frontend Integration Guide
ECOFlow is an Engineering Change Order (ECO) Management System with version control for Product Master Data and Bill of Materials (BoM). It ensures no direct edits to active data, full traceability, and approval-driven workflows.
- Runtime: Node.js + Express + TypeScript
- Database: PostgreSQL with Prisma ORM
- Authentication: JWT (Access + Refresh tokens)
- Real-time: Server-Sent Events (SSE) for notifications
- Separation of Concerns: Draft changes stored separately from active master data
- Version Control: All changes create new versions (no overwrites)
- Role-Based Access Control (RBAC): 4 roles with distinct permissions
- Audit Trail: Every action logged with old/new values
Base Path: /api/auth
| Method | Endpoint | Access | Description | Request Body | Response |
|---|---|---|---|---|---|
POST |
/signup |
Public | Register new user | { email, password, name, roles } |
User object + tokens |
POST |
/login |
Public | User login | { email, password } |
User object + role + access/refresh tokens |
POST |
/refresh |
Public | Refresh access token | { refreshToken } |
New access token |
POST |
/logout |
Public | Logout user | { refreshToken } |
Success message |
GET |
/me |
Private | Get current user info | - | User object with roles |
Frontend Usage:
- Use
/loginto authenticate and store tokens - JWT contains
userIdandrolesfor UI adaptation - Call
/meon app load to verify session - Implement auto-refresh logic for expired tokens
Base Path: /api/users
Access: ADMIN only (all endpoints)
| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
GET |
/ |
Get all users (with pagination/search) | Query: ?page=1&limit=10&search=text |
Array of users + pagination |
GET |
/:id |
Get user by ID | - | User object with roles |
PUT |
/:id |
Update user details | { name, email, roles } |
Updated user |
PATCH |
/:id/status |
Activate/disable user | { status: 'ACTIVE' | 'DISABLED' } |
Updated user |
PATCH |
/:id/password |
Reset user password (admin) | { newPassword } |
Success message |
DELETE |
/:id |
Delete user | - | Success message |
Frontend Usage:
- Admin panel for user management
- Disable users instead of deleting (soft delete)
- Password reset by admin (not self-service)
Base Path: /api/roles
Access: ADMIN only (all endpoints)
| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
PUT |
/users/:id/roles |
Assign multiple roles to user | { roles: ['ENGINEERING', 'APPROVER'] } |
Updated user with roles |
POST |
/users/:id/roles |
Add single role to user | { role: 'ENGINEERING' } |
Updated user with roles |
DELETE |
/users/:id/roles |
Remove single role from user | { role: 'ENGINEERING' } |
Updated user with roles |
GET |
/users/:id/roles |
Get user's roles | - | Array of roles |
Available Roles:
ENGINEERING- Create and modify ECOsAPPROVER- Review and approve ECOsOPERATIONS- View active data onlyADMIN- Full system access
Frontend Usage:
- User can have multiple roles simultaneously
- Frontend should check roles array:
user.roles.includes('ADMIN')
Base Path: /api/products
Access: Mixed (see individual endpoints)
| Method | Endpoint | Access | Description | Request Body | Response |
|---|---|---|---|---|---|
POST |
/ |
ENGINEERING, ADMIN | Create new product | { name } |
Product with v1.0 (DRAFT) |
GET |
/ |
All authenticated | Get all products | Query: ?page=1&limit=10&search=text&archived=false |
Products array + pagination |
GET |
/:id |
All authenticated | Get product by ID | - | Product with all versions |
PUT |
/:id |
ENGINEERING, ADMIN | Update product name | { name } |
Updated product |
DELETE |
/:id |
ADMIN | Delete product | - | Success message |
PATCH |
/:id/archive |
ADMIN | Archive product + cascade | Query: ?cascadeArchive=true |
Success message |
POST |
/:id/versions |
ENGINEERING, ADMIN | Create new version | { version, salePrice, costPrice, attachments } |
New ProductVersion |
PUT |
/versions/:id |
ENGINEERING, ADMIN | Update version | { salePrice, costPrice, attachments } |
Updated ProductVersion |
Product Status Flow: DRAFT → ACTIVE → ARCHIVED
CRITICAL RULES:
- ❌ Cannot edit ACTIVE products directly (must use ECO)
- ❌ Cannot edit ARCHIVED products (read-only)
- ✅ Only DRAFT products can be edited directly
⚠️ Archiving cascades to all versions and BOMs (ifcascadeArchive=true)
Frontend Usage:
// Creating product
POST /api/products { name: "Gear Assembly" }
// Response: { product: { id, name, status: "DRAFT", currentVersion: { version: "v1.0" } } }
// Making product active (via ECO only)
// Archived products cannot be:
// - Edited
// - Selected in new BOMs
// - Selected in new ECOsBase Path: /api/boms
Access: Mixed (see individual endpoints)
| Method | Endpoint | Access | Description | Request Body | Response |
|---|---|---|---|---|---|
POST |
/ |
ENGINEERING, ADMIN | Create BoM | { productVersionId, version, components, operations } |
BoM object |
GET |
/ |
All authenticated | Get all BoMs | Query: ?productId=xxx&page=1&limit=10 |
BoMs array + pagination |
GET |
/:id |
All authenticated | Get BoM by ID | - | BoM with components & operations |
PUT |
/:id |
ENGINEERING, ADMIN | Update BoM metadata | { version, status } |
Updated BoM |
POST |
/:id/components |
ENGINEERING, ADMIN | Add component | { productId, quantity } |
New component |
DELETE |
/:id/components/:componentId |
ENGINEERING, ADMIN | Remove component | - | Success message |
POST |
/:id/operations |
ENGINEERING, ADMIN | Add operation | { name, time, workCenter, sequence } |
New operation |
DELETE |
/:id/operations/:operationId |
ENGINEERING, ADMIN | Remove operation | - | Success message |
BoM Structure:
{
"id": "bom-123",
"productVersionId": "pv-456",
"version": "v1.0",
"status": "ACTIVE",
"components": [
{ "productId": "prod-789", "quantity": 5 }
],
"operations": [
{ "name": "Welding", "time": 30, "workCenter": "WC-01", "sequence": 1 }
]
}CRITICAL RULES:
- ❌ Cannot use ARCHIVED products as components
- ❌ Cannot modify ARCHIVED BOMs (read-only)
- ✅ Each BoM linked to specific ProductVersion
- ✅ Old BoMs preserved after version update (no overwrite)
Frontend Usage:
- Filter components to exclude archived products
- Validate component product status before adding
- Use version dropdown to show BoM history
Base Path: /api/ecos
Access: Role-based (see individual endpoints)
| Method | Endpoint | Access | Description | Request Body | Response |
|---|---|---|---|---|---|
POST |
/ |
ENGINEERING, ADMIN | Create new ECO | { title, type, productId, bomId?, versionUpdate, effectiveDate } |
ECO object |
GET |
/ |
All authenticated | Get all ECOs | Query: ?page=1&limit=10&status=DRAFT&type=PRODUCT |
ECOs array + pagination |
GET |
/:id |
All authenticated | Get ECO by ID | - | ECO with full details |
PUT |
/:id |
ENGINEERING, ADMIN | Update ECO draft | { title, draftData, effectiveDate, versionUpdate } |
Updated ECO |
POST |
/:id/submit |
ENGINEERING, ADMIN | Submit ECO for approval | - | ECO moved to first approval stage |
POST |
/:id/review |
APPROVER, ADMIN | Approve/reject ECO | { action: 'APPROVE'|'REJECT', comments } |
ECO with updated stage |
POST |
/:id/apply |
ADMIN | Apply ECO (final stage) | - | New version created + ECO marked APPLIED |
ECO Types:
PRODUCT- Changes to product pricing/attachmentsBOM- Changes to components/operations
ECO Status Flow:
DRAFT → (submit) → IN_PROGRESS → (approve) → IN_PROGRESS → (approve) → APPROVED → (apply) → APPLIED
↓ (reject)
REJECTED
ECO Draft Data Structure:
{
"draftData": {
"product": {
"salePrice": 150.00,
"costPrice": 100.00,
"attachments": ["url1", "url2"]
},
"bom": {
"components": [
{ "productId": "prod-123", "quantity": 10 }
],
"operations": [
{ "name": "Assembly", "time": 45, "workCenter": "WC-02", "sequence": 1 }
]
}
}
}Version Update Behavior:
versionUpdate: true→ Creates NEW version (e.g., v1.0 → v2.0)versionUpdate: false→ Modifies SAME version (if supported)
CRITICAL RULES:
- ❌ Draft changes DO NOT affect master data until applied
- ✅ Must validate mandatory fields before submission
- ✅ Stage-based approval flow (configurable via Settings)
- ✅ Final stage marks ECO as APPLIED
⚠️ When applied: Old version → ARCHIVED, New version → ACTIVE
Frontend Usage:
// Create ECO
POST /api/ecos {
title: "Update Gear Sale Price",
type: "PRODUCT",
productId: "prod-123",
versionUpdate: true,
effectiveDate: "2026-02-01",
draftData: {
product: { salePrice: 200 }
}
}
// Submit for approval
POST /api/ecos/:id/submit
// Approver reviews
POST /api/ecos/:id/review {
action: "APPROVE",
comments: "Price increase justified"
}
// Admin applies (final)
POST /api/ecos/:id/applyBase Path: /api/comparison
Access: All authenticated users
| Method | Endpoint | Description | Query Params | Response |
|---|---|---|---|---|
GET |
/ecos/:id/comparison |
Get ECO changes diff | - | Old vs New comparison |
GET |
/products/:productId/versions |
Get product version history | - | Array of versions with changes |
GET |
/boms/:bomId/comparison/:oldVersion/:newVersion |
Compare 2 BoM versions | - | Component & operation diffs |
Comparison Response Example:
{
"type": "PRODUCT",
"changes": {
"salePrice": { "old": 100, "new": 150, "change": "+50" },
"costPrice": { "old": 70, "new": 70, "change": "0" },
"attachments": { "added": ["file3.pdf"], "removed": [] }
}
}For BoM:
{
"components": {
"added": [{ "productId": "prod-789", "quantity": 5 }],
"removed": [{ "productId": "prod-456", "quantity": 3 }],
"modified": [
{ "productId": "prod-123", "oldQuantity": 10, "newQuantity": 15 }
]
},
"operations": {
"added": [{ "name": "Polishing", "time": 20 }],
"removed": [],
"modified": [
{ "name": "Welding", "oldTime": 30, "newTime": 45 }
]
}
}Frontend Usage:
- Show before/after comparison in ECO review screen
- Highlight changed fields in red/green
- Use for audit reports
Base Path: /api/reports
Access: Mixed (see individual endpoints)
| Method | Endpoint | Access | Description | Query Params | Response |
|---|---|---|---|---|---|
GET |
/audit-logs |
ADMIN | Get audit trail | ?page=1&limit=50&entityType=ECO&startDate=2026-01-01 |
Audit logs array |
GET |
/eco-stats |
All authenticated | Get ECO statistics | - | Stats object (counts by status/type) |
GET |
/products/:id/version-history |
All authenticated | Product version changelog | - | Version history with changes |
GET |
/product-versions/:id/bom-history |
All authenticated | BoM change history | - | BoM changelog |
GET |
/archived-products |
All authenticated | List archived products | ?page=1&limit=10 |
Archived products array |
GET |
/active-matrix |
All authenticated | Active Product-Version-BoM matrix | - | Matrix of active entities |
Audit Log Structure:
{
"action": "VERSION_CREATE",
"entityType": "PRODUCT_VERSION",
"entityId": "pv-456",
"oldValue": { "version": "v1.0", "salePrice": 100 },
"newValue": { "version": "v2.0", "salePrice": 150 },
"userId": "user-123",
"ecoId": "eco-789",
"stage": "Applied",
"ipAddress": "192.168.1.1",
"createdAt": "2026-01-24T10:30:00Z"
}Tracked Events:
CREATE,UPDATE,DELETEAPPROVE,REJECTARCHIVE,STAGE_TRANSITIONVERSION_CREATE
Frontend Usage:
- Display audit trail in ECO details
- Generate compliance reports
- Track who changed what and when
Base Path: /api/settings
Access: Mixed (read: all, write: ADMIN)
| Method | Endpoint | Access | Description | Request Body | Response |
|---|---|---|---|---|---|
GET |
/stages |
All authenticated | Get all approval stages | - | Array of stages |
GET |
/stages/:id |
All authenticated | Get stage by ID | - | Stage object |
POST |
/stages |
ADMIN | Create approval stage | { name, order, requiresApproval, isFinal } |
New stage |
PUT |
/stages/:id |
ADMIN | Update stage | { name, order, requiresApproval, isFinal } |
Updated stage |
DELETE |
/stages/:id |
ADMIN | Delete stage | - | Success message |
GET |
/stages/next/:currentSequence |
All authenticated | Get next stage in workflow | - | Next stage object |
Approval Stage Structure:
{
"id": "stage-123",
"name": "Engineering Review",
"order": 1,
"requiresApproval": true,
"isFinal": false
}Example Workflow:
- New (order: 0) - Draft creation
- Engineering Review (order: 1, requiresApproval: true)
- Management Approval (order: 2, requiresApproval: true)
- Applied (order: 3, isFinal: true)
Frontend Usage:
- Display workflow progress bar
- Show current stage in ECO list
- Configure stages in admin panel
Base Path: /api/operations
Access: OPERATIONS, ADMIN only
| Method | Endpoint | Description | Query Params | Response |
|---|---|---|---|---|
GET |
/products |
Get ACTIVE products only | ?page=1&limit=10 |
Active products array |
GET |
/products/:id |
Get specific active product | - | Active product details |
GET |
/boms |
Get ACTIVE BOMs only | ?page=1&limit=10 |
Active BOMs array |
GET |
/boms/:id |
Get specific active BoM | - | Active BoM with components/operations |
GET |
/active-matrix |
Active Product-Version-BoM matrix | - | Matrix of usable data |
CRITICAL: These endpoints NEVER return DRAFT or ARCHIVED data
Frontend Usage:
- Operations role sees ONLY production-ready data
- Use for manufacturing planning
- No visibility into pending ECOs or drafts
Base Path: /api/notifications
Access: All authenticated users
| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
GET |
/stream |
SSE stream for real-time notifications | - | EventSource stream |
POST |
/broadcast |
Send notification to all users | { type, title, message, data } |
Success message |
POST |
/users/:userId |
Send notification to specific user | { type, title, message, data } |
Success message |
GET |
/ |
Get user's notifications | ?read=false&page=1 |
Notifications array |
PATCH |
/:id/read |
Mark notification as read | - | Updated notification |
PATCH |
/read-all |
Mark all as read | - | Success message |
DELETE |
/:id |
Delete notification | - | Success message |
Notification Types:
ECO_CREATED,ECO_SUBMITTEDECO_APPROVED,ECO_REJECTED,ECO_APPLIEDSTAGE_CHANGED,APPROVAL_REQUIREDVERSION_CREATED
Frontend Integration:
// Connect to SSE stream
const eventSource = new EventSource('/api/notifications/stream', {
headers: { Authorization: `Bearer ${accessToken}` }
});
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
// Show toast/banner
showNotification(notification);
};This section maps to the requirements specification and ensures complete traceability for frontend developers.
| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| A1 | ✅ PASS | Direct edits blocked on ACTIVE products via status check | product.controller.ts:115-124 |
| A2 | ✅ PASS | Draft data stored in ECO.draftData JSON field |
schema.prisma:183, eco.controller.ts:220 |
| A3 | ✅ PASS | Old versions marked ARCHIVED (never deleted) | eco.controller.ts:47-54 |
| A4 | ✅ PASS | Stage-driven approval with ApprovalStage model | schema.prisma:211-231, eco.controller.ts:450 |
Enforcement Details:
A1 - Prevent Direct Edits:
// In product.controller.ts
if (existingProduct.status === 'ACTIVE') {
res.status(400).json({
message: 'Cannot directly edit active products. Use ECO workflow.'
});
return;
}A2 - Separation of Draft/Applied:
ECO.draftDatastores proposed changes as JSON- Master data remains unchanged until
applyECO()is called - Comparison endpoint compares
draftDatavscurrentVersion
A3 - History Preservation:
- When ECO applied:
status: 'ARCHIVED'(notDELETE) - Archived products queryable:
GET /api/reports/archived-products - Foreign keys use
onDelete: Restrictto prevent cascading deletes
A4 - Consistent Approval:
ApprovalStagedefines workflow (order, requiresApproval, isFinal)ECOApprovaltracks approvals per stage- Cannot skip stages (enforced in
reviewECO())
| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| B1 | ✅ PASS | /api/auth/signup and /api/auth/login exist |
auth.routes.ts:11-20 |
| B2 | ✅ PASS | JWT payload includes userId and roles |
jwt.utils.ts, auth.controller.ts:87 |
| B3 | ✅ PASS | 4 roles defined in UserRole enum | schema.prisma:17-22 |
| B4 | ✅ PASS | authenticate() and authorize() middleware enforced |
auth.middleware.ts, all routes |
| B5 | ✅ PASS | ENGINEERING can create/modify ECOs | eco.routes.ts:15-17 |
| B6 | ✅ PASS | APPROVER can review/approve ECOs | eco.routes.ts:19 |
| B7 | ✅ PASS | OPERATIONS sees ACTIVE data only | operations.routes.ts, operations.controller.ts:20-50 |
| B8 | ✅ PASS | ADMIN has full access + stage configuration | All routes + settings.routes.ts |
Middleware Stack:
// All protected routes use:
router.use(authenticate); // Verify JWT
router.use(authorize('ADMIN', 'ENGINEERING')); // Check rolesRole Enforcement Examples:
ENGINEERING Permissions:
router.post('/', authorize('ENGINEERING', 'ADMIN'), createECO);
router.put('/:id', authorize('ENGINEERING', 'ADMIN'), updateECO);
router.post('/:id/submit', authorize('ENGINEERING', 'ADMIN'), submitECO);APPROVER Permissions:
router.post('/:id/review', authorize('APPROVER', 'ADMIN'), reviewECO);OPERATIONS Permissions:
// In operations.controller.ts
where: { status: 'ACTIVE' } // ONLY active dataFrontend Integration:
// After login, check roles:
const user = jwtDecode(accessToken);
if (user.roles.includes('APPROVER')) {
showApprovalQueue();
}| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| C1 | ✅ PASS | ProductVersion model has all required fields | schema.prisma:86-101 |
| C2 | ✅ PASS | Direct update blocked for ACTIVE products | product.controller.ts:115-124 |
| C3 | ✅ PASS | Archived products read-only + validation checks | product.controller.ts:130-137, bom.controller.ts:16-30 |
Product Schema:
model ProductVersion {
salePrice Float
costPrice Float
attachments Json? // Array of file URLs
status ProductStatus // DRAFT, ACTIVE, ARCHIVED
}Archived Product Restrictions:
Cannot Edit:
if (existingProduct.status === 'ARCHIVED') {
res.status(400).json({
message: 'Cannot edit archived products. Read-only for traceability.'
});
}Cannot Use in BoMs:
// In createBOM()
const archivedProducts = await prisma.product.findMany({
where: { id: { in: componentProductIds }, status: 'ARCHIVED' }
});
if (archivedProducts.length > 0) {
res.status(400).json({
message: `Cannot use archived products: ${names}`
});
}Cannot Use in ECOs:
// Similar validation in createECO()Frontend Usage:
- Filter product dropdown:
status !== 'ARCHIVED' - Show archived flag in product list
- Display "Read-Only" banner for archived products
| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| D1 | ✅ PASS | BoM linked to ProductVersion via productVersionId |
schema.prisma:115-133 |
| D2 | ✅ PASS | Operations controller filters status: 'ACTIVE' |
operations.controller.ts:30 |
| D3 | ✅ PASS | Old BoMs marked ARCHIVED (not deleted) | eco.controller.ts:116-125 |
| D4 | ✅ PASS | BOMComponent and BOMOperation models exist | schema.prisma:135-170 |
| D5 | ✅ PASS | Archived BoM edit blocked | bom.controller.ts:120-129 |
BoM-Version Linking:
model BOM {
productVersionId String
version String // v1.0, v2.0
status BOMStatus // DRAFT, ACTIVE, ARCHIVED
productVersion ProductVersion @relation(...)
@@unique([productVersionId, version]) // One BoM per version
}Active BoM Enforcement:
// In operations.controller.ts
const boms = await prisma.bOM.findMany({
where: { status: 'ACTIVE' } // Only active BOMs
});BoM Preservation: When ECO creates new BoM version:
// Old BoM
await tx.bOM.update({ where: { id: oldBomId }, data: { status: 'ARCHIVED' } });
// New BoM
const newBom = await tx.bOM.create({ data: { ... , status: 'ACTIVE' } });Frontend Usage:
- Show BoM version dropdown
- Filter archived BoMs in selects
- Display "Archived" badge for historical BoMs
| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| E1 | ✅ PASS | ECO model has all required fields | schema.prisma:175-197 |
| E2 | ✅ PASS | Default stage is "New" | schema.prisma:188 |
| E3 | ✅ PASS | Validation before submission | eco.controller.ts:300-330 |
| E4 | ✅ PASS | draftData JSON field stores changes |
schema.prisma:190 |
| E5 | ✅ PASS | versionUpdate boolean controls behavior |
schema.prisma:189, eco.controller.ts:35-40 |
| E6 | ✅ PASS | Stage-based approval logic | eco.controller.ts:450-520 |
| E7 | ✅ PASS | reviewECO() moves to next stage |
eco.controller.ts:487 |
| E8 | ✅ PASS | Final stage marks status: 'APPLIED' |
eco.controller.ts:540 |
| E9 | ✅ PASS | applyECO() archives old, activates new |
eco.controller.ts:47-80 |
| E10 | ✅ PASS | Operations endpoints exclude non-active | operations.controller.ts:20-50 |
ECO Lifecycle:
1. Create (DRAFT):
POST /api/ecos {
title: "Update Sale Price",
type: "PRODUCT",
productId: "prod-123",
versionUpdate: true,
draftData: { product: { salePrice: 200 } }
}
// status: DRAFT, currentStage: "New"2. Submit (IN_PROGRESS):
POST /api/ecos/:id/submit
// Validates mandatory fields
// status: IN_PROGRESS, currentStage: "Engineering Review"3. Review (approval):
POST /api/ecos/:id/review { action: "APPROVE", comments: "..." }
// Moves to next stage OR marks APPROVED if final approval stage4. Apply (APPLIED):
POST /api/ecos/:id/apply
// Creates new version (if versionUpdate: true)
// Archives old version
// status: APPLIEDVersion Update Behavior:
versionUpdate: true (New Version):
// Old: v1.0 → ARCHIVED
// New: v2.0 → ACTIVEversionUpdate: false (Same Version):
// Modifies existing version
// No archive (use carefully!)Draft Data Isolation:
ECO.draftDatadoes NOT touchProductVersionorBOMtables- Comparison endpoint reads from
draftData - Only
applyECO()writes to master tables
Frontend Usage:
// Create ECO with draft changes
const draftData = {
product: {
salePrice: newPrice,
costPrice: newCost
}
};
POST /api/ecos { title, type, productId, versionUpdate: true, draftData };
// Display draft changes in preview
GET /api/comparison/ecos/:id/comparison
// Submit for approval
POST /api/ecos/:id/submit
// Approvers review
POST /api/ecos/:id/review { action: "APPROVE" }
// Admin applies
POST /api/ecos/:id/apply| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| F1 | ✅ PASS | BoM comparison endpoint exists | comparison.routes.ts:12, comparison.controller.ts |
| F2 | ✅ PASS | Product comparison in ECO endpoint | comparison.controller.ts:getECOComparison() |
| F3 | ✅ PASS | Compares currentVersion vs draftData | comparison.controller.ts:45-80 |
Comparison Logic:
Product Changes:
const old = productVersion; // Current active version
const new = eco.draftData.product; // Draft changes
const comparison = {
salePrice: { old: old.salePrice, new: new.salePrice, change: diff },
costPrice: { old: old.costPrice, new: new.costPrice, change: diff },
attachments: { added: [...], removed: [...] }
};BoM Changes:
GET /api/comparison/boms/:bomId/comparison/:oldVersion/:newVersion
const comparison = {
components: {
added: [{ productId, quantity }],
removed: [{ productId, quantity }],
modified: [{ productId, oldQuantity, newQuantity }]
},
operations: {
added: [{ name, time }],
removed: [{ name, time }],
modified: [{ name, oldTime, newTime }]
}
};Frontend Usage:
// Show ECO changes before approval
GET /api/comparison/ecos/:id/comparison
// Render diff UI
{comparison.changes.salePrice && (
<div>
<span className="old">${comparison.changes.salePrice.old}</span>
<span className="arrow">→</span>
<span className="new">${comparison.changes.salePrice.new}</span>
</div>
)}
// BoM version comparison
GET /api/comparison/boms/:bomId/comparison/v1.0/v2.0| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| G1 | ✅ PASS | All events logged via AuditLog model | schema.prisma:261-289 |
| G2 | ✅ PASS | Captures action, old/new values, user, timestamp | audit.controller.ts, multiple controllers |
Audit Log Schema:
model AuditLog {
action AuditAction // CREATE, UPDATE, DELETE, APPROVE, etc.
entityType EntityType // PRODUCT, ECO, BOM, etc.
entityId String
oldValue Json? // Before state
newValue Json? // After state
userId String?
ecoId String?
stage String?
comments String?
ipAddress String?
createdAt DateTime
}Tracked Events:
enum AuditAction {
CREATE, UPDATE, DELETE,
APPROVE, REJECT,
ARCHIVE,
STAGE_TRANSITION,
VERSION_CREATE
}Example Audit Entry:
await tx.auditLog.create({
data: {
userId: req.user.id,
action: 'VERSION_CREATE',
entityType: 'PRODUCT_VERSION',
entityId: newVersion.id,
ecoId: eco.id,
oldValue: { versionId: oldVersion.id, version: 'v1.0' },
newValue: { versionId: newVersion.id, version: 'v2.0' },
comments: `Version created via ECO: ${eco.title}`,
}
});Frontend Usage:
// Display audit trail
GET /api/reports/audit-logs?entityType=ECO&entityId=eco-123
// Show timeline
{auditLogs.map(log => (
<div>
<strong>{log.action}</strong> by {log.user.name}
<span>{log.createdAt}</span>
<div>
Old: {JSON.stringify(log.oldValue)}
New: {JSON.stringify(log.newValue)}
</div>
</div>
))}| Rule ID | Status | Implementation | Evidence |
|---|---|---|---|
| H1 | ✅ PASS | ECO report endpoints exist | report.routes.ts:12, report.controller.ts |
| H2 | ✅ PASS | Version history, BoM history, archived products endpoints exist | report.routes.ts:15-18 |
Available Reports:
1. ECO Report:
GET /api/reports/eco-stats
// Response: { total, draft, inProgress, approved, applied, rejected }2. Product Version History:
GET /api/reports/products/:id/version-history
// Response: [
// { version: "v2.0", createdAt, changes: { salePrice: 150 } },
// { version: "v1.0", createdAt, changes: { salePrice: 100 } }
// ]3. BoM Change History:
GET /api/reports/product-versions/:id/bom-history
// Response: [
// { version: "v2.0", components: [...], operations: [...] },
// { version: "v1.0", components: [...], operations: [...] }
// ]4. Archived Products:
GET /api/reports/archived-products
// Response: [
// { id, name, status: "ARCHIVED", archivedAt, lastVersion }
// ]5. Active Matrix:
GET /api/reports/active-matrix
// Response: [
// {
// product: "Gear Assembly",
// version: "v2.0",
// bom: "BOM-v2.0",
// status: "ACTIVE"
// }
// ]Frontend Usage:
- Admin dashboard with charts
- Export to CSV/PDF
- Historical trend analysis
User (roles: ENGINEERING, APPROVER, OPERATIONS, ADMIN)
├─ createdECOs → ECO[]
├─ approvals → ECOApproval[]
└─ auditLogs → AuditLog[]
Product (status: DRAFT, ACTIVE, ARCHIVED)
├─ versions → ProductVersion[]
├─ currentVersion → ProductVersion
├─ ecos → ECO[]
└─ bomComponents → BOMComponent[]
ProductVersion (version: v1.0, v2.0, ...)
├─ product → Product
├─ boms → BOM[]
└─ currentForProduct → Product
BOM (status: DRAFT, ACTIVE, ARCHIVED)
├─ productVersion → ProductVersion
├─ components → BOMComponent[]
├─ operations → BOMOperation[]
└─ ecos → ECO[]
ECO (type: PRODUCT | BOM, status: DRAFT, IN_PROGRESS, APPROVED, REJECTED, APPLIED)
├─ product → Product
├─ bom → BOM (optional)
├─ creator → User
├─ approvals → ECOApproval[]
├─ auditLogs → AuditLog[]
└─ draftData (JSON)
ApprovalStage (order: 0, 1, 2, ...)
└─ approvals → ECOApproval[]
ECOApproval (status: PENDING, APPROVED, REJECTED)
├─ eco → ECO
├─ stage → ApprovalStage
└─ approver → User
Access Token (expires: 15 minutes):
{
"userId": "user-123",
"email": "john@example.com",
"roles": ["ENGINEERING", "APPROVER"],
"iat": 1706088000,
"exp": 1706088900
}Refresh Token (expires: 7 days):
- Stored in
refresh_tokenstable - Used to generate new access tokens
- Revoked on logout
// 1. Authenticate - Verify JWT
authenticate(req, res, next)
├─ Extract Bearer token
├─ Verify signature
├─ Attach req.user = { id, email, roles }
└─ Next()
// 2. Authorize - Check roles
authorize('ENGINEERING', 'ADMIN')(req, res, next)
├─ Check req.user.roles
├─ If match → Next()
└─ Else → 403 ForbiddenAll authenticated endpoints require:
Authorization: Bearer <access_token>
Content-Type: application/json
Standard Error Format:
{
"status": "error",
"message": "Detailed error message",
"code": "ERROR_CODE" // Optional
}HTTP Status Codes:
200- Success201- Created400- Bad Request (validation error)401- Unauthorized (invalid/missing token)403- Forbidden (insufficient permissions)404- Not Found500- Internal Server Error
// 1. Login
const { user, accessToken, refreshToken } = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
// Store tokens
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
// 2. Decode token to get roles
const decoded = jwtDecode(accessToken);
// decoded.roles = ['ENGINEERING', 'ADMIN']
// 3. Set axios interceptor
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
return config;
});
// 4. Handle token refresh
axios.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken');
const { accessToken } = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken })
});
localStorage.setItem('accessToken', accessToken);
// Retry original request
return axios(error.config);
}
}
);import { jwtDecode } from 'jwt-decode';
const user = jwtDecode(localStorage.getItem('accessToken'));
// Check single role
{user.roles.includes('ENGINEERING') && <CreateECOButton />}
// Check multiple roles
{user.roles.some(r => ['ADMIN', 'APPROVER'].includes(r)) && <ApproveButton />}
// Route protection
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminPanel />
</ProtectedRoute>// Step 1: Create ECO
const createECO = async (data) => {
const response = await fetch('/api/ecos', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: data.title,
type: 'PRODUCT',
productId: data.productId,
versionUpdate: true,
effectiveDate: data.effectiveDate,
draftData: {
product: {
salePrice: data.newPrice,
costPrice: data.newCost
}
}
})
});
return response.json();
};
// Step 2: Preview changes
const previewECO = async (ecoId) => {
const response = await fetch(`/api/comparison/ecos/${ecoId}/comparison`);
const comparison = await response.json();
// Render diff
return (
<div>
<h3>Proposed Changes</h3>
{comparison.changes.salePrice && (
<div>
Sale Price:
<del>${comparison.changes.salePrice.old}</del>
→
<strong>${comparison.changes.salePrice.new}</strong>
</div>
)}
</div>
);
};
// Step 3: Submit for approval
const submitECO = async (ecoId) => {
await fetch(`/api/ecos/${ecoId}/submit`, { method: 'POST' });
// ECO now in approval workflow
};
// Step 4: Approve (APPROVER role)
const approveECO = async (ecoId, comments) => {
await fetch(`/api/ecos/${ecoId}/review`, {
method: 'POST',
body: JSON.stringify({
action: 'APPROVE',
comments: comments
})
});
};
// Step 5: Apply (ADMIN role)
const applyECO = async (ecoId) => {
await fetch(`/api/ecos/${ecoId}/apply`, { method: 'POST' });
// New version created, old version archived
};// WRONG ❌
const products = await fetch('/api/products');
// CORRECT ✅
const products = await fetch('/api/products?archived=false');
// Component filtering
const activeProducts = products.filter(p => p.status !== 'ARCHIVED');
<Select>
{activeProducts.map(p => (
<option value={p.id} key={p.id}>
{p.name} (v{p.currentVersion.version})
</option>
))}
</Select>import { useEffect, useState } from 'react';
const NotificationProvider = () => {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const eventSource = new EventSource('/api/notifications/stream', {
headers: {
Authorization: `Bearer ${localStorage.getItem('accessToken')}`
}
});
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications(prev => [notification, ...prev]);
// Show toast
toast.info(`${notification.title}: ${notification.message}`);
};
eventSource.onerror = () => {
eventSource.close();
};
return () => eventSource.close();
}, []);
return <NotificationBell count={notifications.filter(n => !n.read).length} />;
};// Operations users should ONLY call /api/operations endpoints
const OperationsView = () => {
// CORRECT ✅
const products = await fetch('/api/operations/products');
const boms = await fetch('/api/operations/boms');
const matrix = await fetch('/api/operations/active-matrix');
// WRONG ❌ (Would expose drafts/archived)
// const products = await fetch('/api/products');
return (
<div>
<h2>Active Products (Production Ready)</h2>
{products.map(p => (
<ProductCard
key={p.id}
name={p.name}
version={p.currentVersion.version}
status={p.status} // Always "ACTIVE"
/>
))}
</div>
);
};const AuditTrail = ({ ecoId }) => {
const [logs, setLogs] = useState([]);
useEffect(() => {
const fetchAuditLogs = async () => {
const response = await fetch(
`/api/reports/audit-logs?entityType=ECO&entityId=${ecoId}`
);
setLogs(await response.json());
};
fetchAuditLogs();
}, [ecoId]);
return (
<Timeline>
{logs.map(log => (
<TimelineItem key={log.id}>
<strong>{log.action}</strong> by {log.user.name}
<span>{new Date(log.createdAt).toLocaleString()}</span>
{log.oldValue && (
<div>
<code>Before: {JSON.stringify(log.oldValue, null, 2)}</code>
<code>After: {JSON.stringify(log.newValue, null, 2)}</code>
</div>
)}
</TimelineItem>
))}
</Timeline>
);
};- Never allow direct edits to ACTIVE products/BOMs
- Always use ECO workflow for changes to active data
- Validate product status before adding to BoM
- Prevent archived products from being selected
- Archived data is read-only (no modifications)
- Old versions marked ARCHIVED (never deleted)
- Each ECO creates a new version (if
versionUpdate: true) - Version numbers auto-increment (v1.0 → v2.0)
- Draft changes stored in
ECO.draftData(not in master tables)
- ECO starts in DRAFT status
- Cannot apply ECO without approvals
- Stage progression is sequential (no skipping)
- Final stage marks ECO as APPLIED
- Audit log captures all stage transitions
- ENGINEERING: Create/modify ECOs
- APPROVER: Review/approve ECOs
- OPERATIONS: View ACTIVE data only
- ADMIN: Full access + system configuration
- All changes logged in
audit_logstable - Capture old/new values, user, timestamp
- ECO history linked to audit entries
- IP address tracking (optional but recommended)
- Use
/api/operations/*for OPERATIONS role - Filter archived products in all dropdowns
- Show version history in product details
- Display ECO diff before approval
- Implement real-time notifications via SSE
- Handle token refresh automatically
-
❌ Calling
/api/productsfor OPERATIONS role
✅ Use/api/operations/productsinstead -
❌ Not filtering archived products in selects
✅ Always checkstatus !== 'ARCHIVED' -
❌ Allowing direct product edits for ACTIVE status
✅ Disable edit button, show "Use ECO" message -
❌ Not showing ECO diff before approval
✅ Call/api/comparison/ecos/:id/comparison -
❌ Forgetting to check
versionUpdateflag
✅ Explain to user: new version vs same version -
❌ Not handling stage-based workflow in UI
✅ Show progress bar with current stage -
❌ Missing role checks in frontend routes
✅ Implement<ProtectedRoute allowedRoles={[...]} /> -
❌ Not implementing token refresh logic
✅ Add axios interceptor for 401 responses -
❌ Ignoring audit trail in ECO details
✅ Display timeline of all changes -
❌ Not connecting to notification stream
✅ Implement EventSource for real-time updates
- Risk: Frontend bypasses ECO workflow and calls
PUT /api/products/:idon ACTIVE product - Mitigation: Backend returns 400 error, but frontend should disable edit button
- Risk: Frontend shows features to unauthorized users
- Mitigation: Always check
user.rolesbefore rendering UI elements
- Risk: Partial updates if error occurs mid-operation
- Mitigation: Backend uses
prisma.$transaction()for multi-step operations
- Risk: Duplicate versions or BoMs created
- Mitigation: Database has
@@unique([productId, version])constraints
- Risk: ECO jumps from stage 1 to stage 3
- Mitigation: Backend enforces sequential stage progression
- Risk: User session expires mid-action
- Mitigation: Implement auto-refresh + warn user before expiry
- Risk: Archived products selected in new BoMs/ECOs
- Mitigation: Frontend filters + backend validation
- Risk: Two users edit same ECO simultaneously
- Mitigation: Implement optimistic locking or last-write-wins strategy
# Server
PORT=5000
NODE_ENV=development
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/ecoflow
# JWT
JWT_ACCESS_SECRET=your-access-secret-key-here
JWT_REFRESH_SECRET=your-refresh-secret-key-here
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d
# Frontend
FRONTEND_URL=http://localhost:3000
# Optional: File uploads
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760 # 10MBUse the provided test-api.ps1 script:
# Test authentication
./test-api.ps1 -Endpoint auth/login -Method POST -Body '{"email":"admin@example.com","password":"password"}'
# Test ECO creation
./test-api.ps1 -Endpoint ecos -Method POST -Token $token -Body '{...}'
# Test product listing
./test-api.ps1 -Endpoint products -Method GET -Token $tokenApply migrations:
npx prisma migrate deploySeed database:
npx prisma db seedGenerate Prisma client:
npx prisma generate{
"status": "success",
"message": "Operation completed successfully",
"data": {
// Response data here
}
}{
"status": "error",
"message": "Error description",
"code": "ERROR_CODE" // Optional
}{
"status": "success",
"data": {
"items": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
}
}v1.0.0 (January 24, 2026)
- Initial backend implementation
- All core features implemented
- Role-based access control
- ECO workflow with approval stages
- Version control for products and BOMs
- Audit logging
- Real-time notifications
- Comparison/diff endpoints
- Operations view endpoints
For issues or questions about the backend API, please contact the development team or refer to the codebase documentation in /backend/src.
© 2026 ECOFlow Engineering Change Order System