Skip to navigation

Create a CRUD API for Documents and the new status field in the DB

This document outlines the implementation stories and acceptance criteria for building a complete CRUD (Create, Read, Update, Delete) API for the /api/v1/document endpoint, following the established patterns from the Clients API.

Current Status

Database Schema Update Required 🔧

The Documents table needs a Status column added:

-- Add Status column to Documents table
ALTER TABLE [dbo].[Documents]
ADD [Status] VARCHAR(50) NOT NULL DEFAULT 'processing';
-- Add check constraint for valid statuses (optional but recommended)
ALTER TABLE [dbo].[Documents]
ADD CONSTRAINT CK_Documents_Status
CHECK ([Status] IN ('processing', 'completed', 'failed', 'pending', 'archived'));

Current Table Schema:

CREATE TABLE [dbo].[Documents] (
    [BlobPath] VARCHAR(800) NOT NULL,
    [ClientId] BIGINT NOT NULL,
    [DateUploaded] DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET(),
    [DocumentHash] VARCHAR(48) UNIQUE NOT NULL,
    [DocumentId] BIGINT IDENTITY(1,1) PRIMARY KEY,
    [Name] VARCHAR(200) NOT NULL,
    [Status] VARCHAR(50) NOT NULL DEFAULT 'processing', -- NEW FIELD
    FOREIGN KEY ([ClientId]) REFERENCES [dbo].[Clients]([ClientId])
);

Existing Components ✅

  • ✅ Database model with basic operations (document.model.ts)

  - getDocumentByID(documentId) - Retrieve document by ID

  - getDocumentByHash(documentHash) - Find document by hash

  - createDocument(newDocument) - Insert new document record

  • ✅ Type definitions (document.types.ts)

  - Document interface

  - NewDocument interface

Missing Components ❌

  • ❌ Database schema migration for Status column

  • ❌ Route definitions (document.route.ts)

  • ❌ Controller layer (document.controller.ts)

  • ❌ Service layer (document.service.ts)

  • ❌ Schema definitions for validation (document.schema.ts)

  • ❌ Update and Delete model methods

  • ❌ Pagination support for listing documents

  • ❌ Filter capabilities

  • ❌ Unit tests

  • ❌ Integration with main routes


Implementation Stories

Story 1: Database Migration - Add Status Column (1 point)

Priority: 🔥 Critical - Foundation  

Status: ⚠️ Needs Implementation

Description: Add the Status column to the Documents table to track document processing state using Flyway migration.

Implementation: Create Flyway migration script

Flyway Migration File: src/main/resources/db/migration/V{next_version}__add_status_to_documents.sql

File Naming Convention:

  • Use next sequential version number (e.g., V1.2__add_status_to_documents.sql)

  • Prefix: V (for versioned migration)

  • Version: Sequential number with underscores

  • Separator: Double underscore __

  • Description: Lowercase with underscores

Migration SQL (V{X.X}__add_status_to_documents.sql):

-- Flyway Migration: Add Status column to Documents table
-- Version: V{X.X}
-- Description: Add Status VARCHAR(50) field to track document processing state
-- Author: [Your Name]
-- Date: 2026-02-04
-- Add Status column with default value
ALTER TABLE [dbo].[Documents]
ADD [Status] VARCHAR(50) NOT NULL DEFAULT 'processing';
-- Add index for status filtering to improve query performance
CREATE NONCLUSTERED INDEX IX_Documents_Status
ON [dbo].[Documents]([Status])
INCLUDE ([DocumentId], [ClientId], [DateUploaded]);
-- Migration complete
-- All existing records will have status = 'processing'

Rollback Script (Optional - U{X.X}__add_status_to_documents.sql for undo migrations):

-- Flyway Undo Migration: Remove Status column from Documents table
-- Version: U{X.X}
-- Description: Rollback status column addition
-- Drop index first
DROP INDEX IF EXISTS IX_Documents_Status ON [dbo].[Documents];
-- Drop Status column
ALTER TABLE [dbo].[Documents]
DROP COLUMN [Status];

Status Field Characteristics:

  • Type: VARCHAR(50) - Free-form text, not constrained to specific values

  • Default: 'processing' for new documents

  • Usage: Can be set to any arbitrary string value based on business needs

  • Common Examples: 'processing', 'completed', 'failed', 'pending', 'archived', 'in-review', 'approved', etc.

  • No Constraints: System does not enforce specific status values, allowing flexibility for different workflows

Flyway Configuration (flyway.conf or environment variables):

# Flyway configuration for migrations
flyway.url=jdbc:sqlserver://localhost:1433;databaseName=YourDatabase
flyway.user=your_db_user
flyway.password=your_db_password
flyway.schemas=dbo
flyway.locations=filesystem:./src/main/resources/db/migration
flyway.baselineOnMigrate=true

Testing the Migration:

# Validate migration syntax
flyway validate
# Check migration status
flyway info
# Run the migration
flyway migrate
# Rollback (if using undo migrations)
flyway undo

Verification Query:

-- Verify Status column was added successfully
SELECT
    COLUMN_NAME,
    DATA_TYPE,
    CHARACTER_MAXIMUM_LENGTH,
    IS_NULLABLE,
    COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Documents'
AND COLUMN_NAME = 'Status';
-- Verify index was created
SELECT
    i.name AS IndexName,
    i.type_desc AS IndexType,
    c.name AS ColumnName
FROM sys.indexes i
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE i.object_id = OBJECT_ID('dbo.Documents')
AND i.name = 'IX_Documents_Status';
-- Verify existing records have default status
SELECT TOP 10 DocumentId, Name, Status FROM Documents;

Acceptance Criteria:

  • ✅ Flyway migration script created with proper naming convention

  • ✅ Status column added to Documents table

  • ✅ Default value set to ‘processing’

  • ✅ Column is NOT NULL

  • ✅ Column allows arbitrary string values (no constraints)

  • ✅ Index created for query performance

  • ✅ Existing records get default ‘processing’ status

  • ✅ Migration tested on development database

  • ✅ Migration runs successfully via flyway migrate

  • ✅ Migration is idempotent (can be validated multiple times)

  • ✅ Rollback script created (optional but recommended)

  • ✅ Migration documented in version control

Dependencies: None (Database prerequisite)

Estimated Effort: 45 minutes (including Flyway setup and testing)


Story 2: Update Type Definitions and Filters (1 point)

Priority: 🔥 Critical - Foundation  

Status: ⚠️ Needs Implementation

Description: Extend the existing type system to support CRUD operations, filtering, and the new status field.

Implementation Location: src/api/v1/document/document.types.ts

Required Interfaces:

// Update existing file to add:
export interface DocumentUpdateData {
  name?: string;
  blobPath?: string;
  status?: string; // NEW: Allow status updates
  // Note: clientId and documentHash should not be updatable
}
export interface DocumentFilters {
  clientId?: number;
  name?: string;
  status?: string; // NEW: Filter by status
  dateFrom?: string; // ISO date string
  dateTo?: string;   // ISO date string
}
export interface DocumentResponse {
  documentId: number;
  blobPath: string;
  clientId: number;
  dateUploaded: string;
  documentHash: string;
  name: string;
  status: string; // NEW: Include status in response (arbitrary string)
}
// NEW: Common status examples (not exhaustive or enforced)
export const DOCUMENT_STATUS = {
  PROCESSING: 'processing',
  COMPLETED: 'completed',
  FAILED: 'failed',
  PENDING: 'pending',
  ARCHIVED: 'archived',
  // Note: Applications can use any status values - these are just common examples
} as const;

Acceptance Criteria:

  • ✅ DocumentUpdateData interface includes optional status field (string type)

  • ✅ DocumentFilters interface supports filtering by status (string type)

  • ✅ DocumentResponse interface includes status field (string type)

  • ✅ Status constants defined as common examples (not enforced)

  • ✅ Status field accepts arbitrary string values

  • ✅ All interfaces properly exported

  • ✅ TypeScript strict mode compliance

  • ✅ No use of any type

  • ✅ Document and NewDocument interfaces updated to include status

Dependencies: Story 1 (Database Migration)

Estimated Effort: 45 minutes


Story 3: Expand Document Model with CRUD Operations (3 points)

Priority: 🔥 Critical - Foundation  

Status: ⚠️ Needs Implementation

Description: Add missing CRUD operations to the DocumentModel class following the Repository pattern, including support for the new status field.

Implementation Location: src/api/v1/document/document.model.ts

Required Methods:

export default class DocumentModel {
  // EXISTING METHODS TO UPDATE:
  // Update these to include Status field in SELECT statements
 
  // NEW: List documents with pagination and filters
  static async getDocuments(
    pagination: PaginationParams,
    filters: DocumentFilters = {}
  ): Promise<{
    total: number;
    items: Document[];
    limit: number;
    offset: number;
  }>;
  // NEW: Update document (including status)
  static async updateDocument(
    documentId: number,
    updateData: DocumentUpdateData
  ): Promise<boolean>;
  // NEW: Delete document (soft or hard delete)
  static async deleteDocument(documentId: number): Promise<boolean>;
  // NEW: Get documents by client ID
  static async getDocumentsByClientId(
    clientId: number,
    pagination: PaginationParams
  ): Promise<{
    count: number;
    items: Document[];
    limit: number;
    offset: number;
  }>;
 
  // NEW: Update document status specifically
  static async updateDocumentStatus(
    documentId: number,
    status: string
  ): Promise<boolean>;
}

SQL Queries Required:

  1. Update Existing Methods to Include Status:
-- Update getDocumentByID, getDocumentByHash, and createDocument
SELECT
  DocumentId AS documentId,
  BlobPath AS blobPath,
  ClientId AS clientId,
  DateUploaded AS dateUploaded,
  DocumentHash AS documentHash,
  Name AS name,
  Status AS status  -- ADD THIS LINE
FROM Documents
WHERE ...;
  1. Get Documents with Pagination and Filters:
-- Count query
SELECT COUNT(*) as total
FROM Documents
WHERE [dynamic WHERE clauses based on filters];
-- Data query
SELECT
  DocumentId AS documentId,
  BlobPath AS blobPath,
  ClientId AS clientId,
  DateUploaded AS dateUploaded,
  DocumentHash AS documentHash,
  Name AS name,
  Status AS status
FROM Documents
WHERE [dynamic WHERE clauses]
ORDER BY DateUploaded DESC
OFFSET @offset ROWS
FETCH NEXT @limit ROWS ONLY;
  1. Update Document (including status):
UPDATE Documents
SET [dynamic SET clauses based on updateData]
    -- Can include: Name = @name, BlobPath = @blobPath, Status = @status
WHERE DocumentId = @documentId;
-- Example with all fields:
UPDATE Documents
SET Name = @name,
    BlobPath = @blobPath,
    Status = @status
WHERE DocumentId = @documentId;
  1. Update 4ocument Status (specific method):
UPDATE Documents
SET Status = @status
WHERE DocumentId = @documentId;
  1. Delete Document (consider soft delete using status):
-- Option 1: Hard delete
DELETE FROM Documents WHERE DocumentId = @documentId;
-- Option 2: Soft delete using status (RECOMMENDED)
UPDATE Documents
SET Status = 'archived'
WHERE DocumentId = @documentId;
  1. Create Document (ensure status is set):
INSERT INTO Documents (BlobPath, ClientId, DateUploaded, DocumentHash, Name, Status)
OUTPUT INSERTED.DocumentId AS documentId,
       INSERTED.BlobPath AS blobPath,
       INSERTED.ClientId AS clientId,
       INSERTED.DateUploaded AS dateUploaded,
       INSERTED.DocumentHash AS documentHash,
       INSERTED.Name AS name,
       INSERTED.Status AS status
VALUES (@blobPath, @clientId, @dateUploaded, @documentHash, @name, @status);
-- Note: @status defaults to 'processing' if not provided

Acceptance Criteria:

  • ✅ All existing SELECT queries updated to include Status field

  • ✅ getDocuments() supports pagination (limit/offset)

  • ✅ getDocuments() supports filters (clientId, name pattern, status, date range)

  • ✅ Dynamic WHERE clause construction based on provided filters

  • ✅ SQL injection protection via parameterized queries

  • ✅ updateDocument() can update status along with other fields

  • ✅ updateDocument() only updates provided fields (dynamic SET clause)

  • ✅ updateDocumentStatus() provides targeted status updates

  • ✅ createDocument() sets default status to ‘processing’

  • ✅ deleteDocument() returns false if document not found

  • ✅ Consider soft delete using ‘archived’ status instead of hard delete

  • ✅ getDocumentsByClientId() includes pagination and status

  • ✅ All database column names properly mapped to camelCase

  • ✅ Error handling for database failures

  • ✅ Methods return null/false appropriately when records not found

Dependencies: Story 1 (Database Migration), Story 2 (Type Definitions)

Estimated Effort: 3-4 hours


Story 3: Create Document Service Layer (2 points)

Priority: 🔥 Critical  

Status: ⚠️ Needs Implementation

Description: Implement service layer to handle business logic, validation, and error handling.

Implementation Location: src/api/v1/document/document.service.ts

Required Implementation:

import { log } from '../../../internal/app.js';
import DocumentModel from './document.model.js';
import { NewDocument, DocumentUpdateData, DocumentFilters } from './document.types.js';
import type { PaginationParams } from '../../../types/pagination.js';
interface ConflictError extends Error {
  code: string;
}
const DocumentService = {
  async getDocuments(
    pagination: PaginationParams,
    filters: DocumentFilters = {}
  ) {
    const { total, items, limit, offset } =
      await DocumentModel.getDocuments(pagination, filters);
    return { total, items, limit, offset };
  },
  async getDocumentById(documentId: number) {
    const document = await DocumentModel.getDocumentByID(documentId);
    if (!document) {
      log.warn('[DocumentService.getDocumentById] Document not found', { documentId });
      throw new Error('Document not found');
    }
    return document;
  },
  async getDocumentsByClientId(
    clientId: number,
    pagination: PaginationParams
  ) {
    const { count, items, limit, offset } =
      await DocumentModel.getDocumentsByClientId(clientId, pagination);
    return { count, items, limit, offset };
  },
  async createDocument(newDocument: NewDocument) {
    // Check for duplicate hash
    const existing = await DocumentModel.getDocumentByHash(
      newDocument.documentHash
    );
   
    if (existing) {
      log.warn('[DocumentService.createDocument] Document hash already exists', {
        documentHash: newDocument.documentHash,
        existingDocumentId: existing.documentId,
      });
      const conflictError: ConflictError = new Error(
        'A document with this hash already exists'
      ) as ConflictError;
      conflictError.code = 'CONFLICT';
      throw conflictError;
    }
    const document = await DocumentModel.createDocument(newDocument);
    if (!document) {
      throw new Error('Failed to create document');
    }
   
    log.info('[DocumentService.createDocument] Document created successfully', {
      documentId: document.documentId,
      name: document.name,
    });
   
    return document;
  },
  async updateDocument(documentId: number, updateData: DocumentUpdateData) {
    // Validate that at least one field is provided
    const validFields = Object.keys(updateData).filter(
      key => updateData[key as keyof DocumentUpdateData] !== undefined
    );
   
    if (validFields.length === 0) {
      throw new Error('No valid fields provided for update');
    }
    // Check if document exists
    const document = await DocumentModel.getDocumentByID(documentId);
    if (!document) {
      log.warn('[DocumentService.updateDocument] Document not found', { documentId });
      throw new Error('Document not found');
    }
2 (Types), Story 3 (Model)
**Estimated Effort**: 2-3 hours
---
### **Story 5ew Error('Failed to update document');
    }
    log.info('[DocumentService.updateDocument] Document updated successfully', {
      documentId,
      updatedFields: validFields,
    });
    return true;
  },
  async deleteDocument(documentId: number) {
    // Check if document exists
    const document = await DocumentModel.getDocumentByID(documentId);
    if (!document) {
      log.warn('[DocumentService.deleteDocument] Document not found', { documentId });
      throw new Error('Document not found');
    }
    const deleted = await DocumentModel.deleteDocument(documentId);
    if (!deleted) {
      log.error('[DocumentService.deleteDocument] Failed to delete document', {
        documentId,
      });
      throw new Error('Failed to delete document');
    }
    log.info('[DocumentService.deleteDocument] Document deleted successfully', {
      documentId,
    });
    return true;
  },
};
export default DocumentService;

Acceptance Criteria:

  • ✅ All CRUD operations implemented

  • ✅ Business logic validation (duplicate hash check)

  • ✅ Proper error handling with descriptive messages

  • ✅ Logging at info level for successful operations

  • ✅ Logging at warn level for not found scenarios

  • ✅ Logging at error level for failures

  • ✅ No sensitive data (PII) logged

  • ✅ ConflictError type for duplicate hash scenarios

  • ✅ Validation for empty update data

Dependencies: Story 1 (Types), Story 2 (Model)

Estimated Effort: 2-3 hours


Story 4: Create Document Controller (2 points)

Priority: 🔥 Critical  

Status: ⚠️ Needs Implementation

Description: Implement controller layer to handle HTTP request/response mapping.

Implementation Location: src/api/v1/document/document.controller.ts

Required Implementation:

import { log } from '../../../internal/app.js';
import type { PaginationParams } from '../../../types/pagination.js';
import DocumentService from './document.service.js';
import { NewDocument, DocumentUpdateData, DocumentFilters } from './document.types.js';
const DocumentController = {
  async getDocuments(
    pagination: PaginationParams,
    filters: DocumentFilters = {}
  ) {
    log.debug('[DocumentController.getDocuments] Fetching documents', {
      pagination,
      filters,
    });
   
    const { total, items, limit, offset } =
      await DocumentService.getDocuments(pagination, filters);
   
    log.debug('[DocumentController.getDocuments] Retrieved documents', {
      total,
      count: items.length,
    });
   
    return { total, items, limit, offset };
  },
  async getDocumentById(documentId: number) {
    log.debug('[DocumentController.getDocumentById] Fetching document', {
      documentId,
    });
   
    const document = await DocumentService.getDocumentById(documentId);
   
    log.debug('[DocumentController.getDocumentById] Retrieved document', {
      documentId,
    });
   
    return document;
  },
  async getDocumentsByClientId(
    clientId: number,
    pagination: PaginationParams
  ) {
    log.debug('[DocumentController.getDocumentsByClientId] Fetching documents for client', {
      clientId,
      pagination,
    });
   
    const { count, items, limit, offset } =
      await DocumentService.getDocumentsByClientId(clientId, pagination);
   
    log.debug('[DocumentController.getDocumentsByClientId] Retrieved documents', {
      clientId,
      count,
    });
   
    return { count, items, limit, offset };
  },
  async createDocument(newDocument: NewDocument) {
    log.info('[DocumentController.createDocument] Creating new document', {
      name: newDocument.name,
      clientId: newDocument.clientId,
    });
   
    const document = await DocumentService.createDocument(newDocument);
   
    log.info('[DocumentController.createDocument] Document created successfully', {
      documentId: document.documentId,
      name: document.name,
    });
   
    return document;
  },
  async updateDocument(documentId: number, updateData: DocumentUpdateData) {
    log.info('[DocumentController.updateDocument] Updating document', {
      documentId,
      fields: Object.keys(updateData),
    });
   
    await DocumentService.updateDocument(documentId, updateData);
   
    log.info('[DocumentController.updateDocument] Document updated successfully', {
      documentId,
    });
   
    return true;
  },
  async deleteDocument(documentId: number) {
    log.info('[DocumentController.deleteDocument] Deleting document', {
      documentId,
    });
   
    await DocumentService.deleteDocument(documentId);
   
    log.info('[DocumentController.deleteDocument] Document deleted successfully', {
      documentId,
    });
   
    return true;
  },
};
export default DocumentController;

Acceptance Criteria:

  • ✅ All controller methods implemented

  • ✅ Debug logging for data retrieval operations

  • ✅ Info logging for data mutation operations

  • ✅ Proper type annotations

  • ✅ Request/response transformation if needed

  • ✅ No business logic in controller (delegated to service)

Dependencies: Story 4 (Service)

Estimated Effort: 1-2 hours


Story 6: Create Document Schema Definitions (3 points)

Priority: 🔥 Critical  

Status: ⚠️ Needs Implementation

Description: Define JSON schemas for request validation and OpenAPI documentation.

Implementation Location: src/api/v1/document/document.schema.ts

Required Implementation:

Acceptance Criteria:

  • ✅ All CRUD endpoints have schema definitions

  • ✅ Input validation for required fields

  • ✅ Response schemas for all status codes

  • ✅ OpenAPI/Swagger compatible format

  • ✅ Security schemes defined (bearerAuth)

  • ✅ Proper tags for documentation grouping

  • ✅ Field length and type constraints

  • ✅ Description for each field

Dependencies: Story 1 (Types)

Estimated Effort: 2-3 hours


Story 6: Create Document Routes (5 points)

Priority: 🔥 Critical  

Status: ⚠️ Needs Implementation

Description: Implement Fastify route handlers with authentication, authorization, and error handling.

Implementation Location: src/api/v1/document/document.route.ts

Required Implementation:

Acceptance Criteria:

  • ✅ Document routes imported

  • ✅ Routes registered with Fastify instance

  • ✅ Route registration follows existing patterns

  • ✅ Application starts without errors

  • ✅ Routes visible in OpenAPI documentation

Dependencies: Story 6 (Routes)

Estimated Effort: 15 minutes


Story 8: Add Scopes for Document Operations (1 point)

Priority: 🔥 Critical  

Status: ⚠️ Needs Implementation

Estimated Effort: 10 minutes


**Story 10

const SCOPES = {

  // … existing scopes …

 

  // Document scopes

  DOCUMENTS_READ: ‘documents.read’,

  DOCUMENTS_WRITE: ‘documents.write’,

 

  // … other scopes …

};

**Acceptance Criteria**:
- ✅ `DOCUMENTS_READ` scope defined
- ✅ `DOCUMENTS_WRITE` scope defined
- ✅ Scopes follow naming convention
- ✅ Scopes exported properly
**Dependencies**: None (parallel with Story 6)
**Estimated Effort**: 10 minutes
---
### **Story 9: Unit Tests for Document Model** (3 points)
**Priority**: 🟡 **High**  
**Status**: ⚠️ **Needs Implementation**
**Description**: Write comprehensive unit tests for DocumentModel class.
**Implementation Location**: `src/api/v1/document/tests/document.model.test.ts`
**Test Coverage Requirements**:
describe('DocumentModel', () => {
  describe('getDocumentByID', () => {
    it('should return document when found', async () => {});
    it('should return null when document not found', async () => {});
    it('should handle database errors', async () => {});
  });
  describe('getDocumentByHash', () => {
    it('should return docstatus', async () => {});
    it('should filter by ument when hash exists', async () => {});
    it('should return null when hash not found', async () => {});
  });
  describe('createDocument', () => {
    it('should create document successfully', async () => {});
    it('should return created document with ID', async () => {});
    it('should handle duplicate hash errors', async () => {});
  });
  describe('getDocuments', () => {
    it('should return paginated documents', async () => {});
    it('should update status independently', async () => {});
    it('should return false when document not found', async () => {});
  });
 
  describe('updateDocumentStatus', () => {
    it('should update status to completed', async () => {});
    it('should update status to failed', async () => {});
    it('should filter by clientId', async () => {});
    it('should filter by name pattern', async () => {});
    it('should filter by date range', async () => {});
    it('should handle empty results', async () => {});
    it('should respect limit and offset', async () => {});
  });
  describe('updateDocument', () => {
    it('should update document successfully', async () => {});
    it('should update only provided fields', async () => {});
    it('should return false when document not found', async () => {});
  }); including status-related methods
- ✅ Status filtering tests included
- ✅ Status update tests included
- ✅ Default status ('processing') tested in create operations
- ✅ Edge cases covered (nulls, empty results)
- ✅ Error scenarios tested
- ✅ Database mocking implemented
- ✅ Tests follow AAA pattern (Arrange, Act, Assert)
- ✅ Descriptive test names
- ✅ No hardcoded test data in production code
**Dependencies**: Story 3 (Model)
**Estimated Effort**: 4-5 hours
---
### **Story 11
**Acceptance Criteria**:
- ✅ 85%+ code coverage for model layer
- ✅ All public methods tested
- ✅ Edge cases covered (nulls, empty results)
- ✅ Error scenarios tested
- ✅ Database mocking implemented
- ✅ Tests follow AAA pattern (Arrange, Act, Assert)
- ✅ Descriptive test names
- ✅ No hardcoded test data in production code
**Dependencies**: Story 2 (Model)
**Estimated Effort**: 4-5 hours
---
### **Story 10: Unit Tests for Document Service** (3 points)
**Priority**: 🟡 **High**  
**Status**: ⚠️ **Needs Implementation**
**Description**: Write comprehensive unit tests for DocumentService.
**Implementation Location**: `src/api/v1/document/tests/document.service.test.ts`
**Test Coverage Requirements**:
describe('DocumentService', () => {
  describe('getDocuments', () => {
    it('should return paginated documents', async () => {});
    it('should pass filters to model', async () => {});
  });
update status successfully', async () => {});
    it('should
  describe('getDocumentById', () => {
    it('should return document when found', async () => {});
    it('should throw error when not found', async () => {});
  });
  describe('createDocument', () => {
    it('should create document successfully', async () => {});
    it('should throw ConflictError when hash exists', async () => {});
    it('should check for duplicate before creating', async () => {});
    it('should log creation success', async () => {});
  });
  describe('updateDocument', () => {
    it('should update document successfully', async () => {});
    it('should throw error when no fields provided', async () => {});
    it('should throw error when document not found', async () => {});
    it('should validate document exists before update', async () => {});
    it('should log update success', async () => {});
  });
 including status handling
- ✅ Status update validation tested
- ✅ Error handling validated
- ✅ Logging verified (using spies/mocks)
- ✅ Model interaction tested
- ✅ ConflictError scenarios covered
- ✅ Mock dependencies properly
**Dependencies**: Story 4 (Service)
**Estimated Effort**: 4-5 hours
---
### **Story 12
**Acceptance Criteria**:
- ✅ 85%+ code coverage for service layer
- ✅ All business logic tested
- ✅ Error handling validated
- ✅ Logging verified (using spies/mocks)
- ✅ Model interaction tested
- ✅ ConflictError scenarios covered
- ✅ Mock dependencies properly
**Dependencies**: Story 3 (Service)
**Estimated Effort**: 4-5 hours
---
### **Story 11: Integratistatus', async () => {});
    it('should filter by on Tests for Document API** (5 points)
**Priority**: 🟡 **High**  
**Status**: ⚠️ **Needs Implementation**
**Description**: Write end-to-end integration tests for document API endpoints.
**Implementation Location**: `src/api/v1/document/tests/document.integration.test.ts`
**Test Coverage Requirements**:
describe('Document API Integration Tests', () => {
  describe('GET /api/v1/documents', () => {
    it('should return 401 without authentication', async () => {});
    it('should return 200 with valid token', async () => {});
    it('should support pagination', async () => {});
    it('should filter by clientId', async () => {});
    it('should filter by name', async () => {});
    it('should filter by date range', async () => {});
    it('should enforce role-based access', async () => {});
  });
  describe('GET /api/v1/documents/:documentId', () => {
    it('should return 404 for non-existent document', async () => {});
    it('should return 200 with valid documentId', async () => {});
    it('should require authentication', async () => {});
  });
  describe('GET /api/v1/clients/:clientId/documents', () => {
    it('should return documents for client', async () => {});
    it('should update document status', async () => {});
    it('should update status from processing to completed', async () => {});
    it('should update status from processing to failed', async () => {});
    it('should return empty array for client with no documents', async () => {});
    it('should support pagination', async () => {});
  });
  describe('POST /api/v1/documents', () => {
    it('should create document with 201 status', async () => {});
    it('should return 409 for duplicate hash', async () => {});
    it('should return 400 for invalid data', async () => {});
    it('should require authentication', async () => {});
    it('should enforce write scope', async () => {});
  });
  deStatus field behavior validated across all operations
- ✅ Status filtering tested
- ✅ Status transitions tested (processing → completed/failed)
- ✅ Default status 'processing' verified on document creation
- ✅ Authentication scenarios covered
- ✅ Authorization (roles and scopes) tested
- ✅ HTTP status codes validated
- ✅ Request/response bodies validated (including status field)
- ✅ Database state verified after operations
- ✅ Test database used (not production)
- ✅ Cleanup after each test
- ✅ Test data fixtures created
**Dependencies**: Story 7 (Routes), Story 8 (Registration)
**Estimated Effort**: 6-8 hours
---
### **Story 13riteria**:
- ✅ All endpoints tested end-to-end
- ✅ Authentication scenarios covered
- ✅ Authorization (roles and scopes) tested
- ✅ HTTP status codes validated
- ✅ Request/response bodies validated
- ✅ Database state verified after o with status field
- Document status field usage and valid values
- Document status transitions and lifecycle
- Document authentication requirements
- Add error response examples
- Update tags and categories
**Acceptance Criteria**:
- ✅ All CRUD endpoints documented
- ✅ Status field documented in all relevant schemas
- ✅ Status enum values documented ('processing', 'completed', 'failed', 'pending', 'archived')
- ✅ Status lifecycle and transitions explained
- ✅ Examples showing status usage (create with default, update status)
- ✅ Request schemas defined
- ✅ Response schemas defined
- ✅ Security requirements documented
- ✅ Examples provided for common scenarios including status changes
- ✅ Documentation generated and accessible via Swagger UI
**Dependencies**: Story 7plementation**
**Description**: Update OpenA2 points
### **Estimated Timeline**:
- Database migration: 0.5 day
- Critical stories (MVP): 3-4 days
- High priority (Testing): 2-3 days
- Medium priority (Documentation): 0.5 day
- **Total**: ~6-8 days for one developer
### **Implementation Order**:
1. Story 1: Database Migration (Add Status Column) ✅ **CRITICAL FIRST**
2. Story 2: Type Definitions ✅ Foundation
3. Story 9: Add Scopes ✅ Foundation
4. Story 3: Model Expansion ✅ Foundation
5. Story 4: Service Layer
6. Story 5: Controller Layer
7. Story 6: Schema Definitions
8. Story 7: Route Handlers
9. Story 8: Route Registration
10. Story 10: Model Tests
11. Story 11: Service Tests
12. Story 12: Integration Tests
13. Story 13: Documentation
### **Critical Dependencies**:

Story 1 (Database Migration)

    ↓

Story 2 (Types)

    ↓

Story 3 (Model)

    ↓

Story 4 (Service)

    ↓

Story 5 (Controller)

    ↓

Story 6 (Schema) + Story 7 (Routes)

    ↓

Story 8 (Registration)

    ↓

Story 10, 11, 12 (Tests)

    ↓

Story 13 2: Model Expansion ✅ Foundation

  1. Story 3: Service Layer

  2. Story 4: Controller Layer

  3. Story 5: Schema Definitions

  4. Story 6: Route Handlers

  5. Story 7: Route Registration

  6. Story 9: Model Tests

  7. Story 10: Service Tests

  8. Story 11: Integration Tests

  9. Story 12: Documentation

Critical Dependencies:

Story 1 (Types)
    ↓
Story 2 (Model)
    ↓
Story 3 (Service)
    ↓
Story 4 (Controller)
    ↓
Story 5 (Schema) + Story 6 (Routes)
    ↓
Story 7 (Registration)
    ↓
Story 9, 10, 11 (Tests)
    ↓
Story 12 (Docs)

Definition of Done (DoD):

For each story to be considered complete, it must meet:

  • ✅ All acceptance criteria met

  • ✅ Code follows TypeScript and project standards

  • ✅ No ESLint errors or warnings

  • ✅status` - Filter by status (‘processing’, ‘completed’, ‘failed’, ‘pending’, ‘archived’)

  • ` Passes Prettier formatting

  • ✅ No use of any type (unless documented exception)

  • ✅ Proper error handling implemented

  • ✅ No PII or sensitive data in logs

  • ✅ Unit tests written with 85%+ coverage

  • ✅ Integration tests pass

  • ✅ Code reviewed and approved

  • ✅ Documentation updated

  • ✅ Deployed to development environment


API Endpoint Summary

Document Endpoints

| Method | Endpoint | Description | Auth | Role |

|--------|----------|-------------|------|------|

| GETStatus Field**: New required field to track document processing state

   - Default value: 'processing' for all new documents

   - Valid values: 'processing', 'completed', 'failed', 'pending', 'archived'

   - Supports status transitions throughout document lifecycle

  1. Hash Uniqueness: Documents are identified uniquely by their hash to prevent duplicate uploads

  2. Soft Delete via Status: Use 'archived' status instead of hard delete for audit purposes

  3. Immutable Fields: clientId and documentHash are not updatable after creation (status IS updatable)

  4. Administrator-Only Mutations: Only admins can update/delete to prevent accidental data loss

  5. Pagination: Default limit of 250 matches other APIs in the system

  6. Status Filtering: Users can filter documents by status for workflow management Admin |

| DELETE | /api/v1/documents/:documentId | Delete document | Required | Admin |

Query Parameters (GET /api/v1/documents):

  • `limStatus Lifecycle and Workflows**:

Typical Document Status Flow:

pending → processing → completed
                    ↓
                  failed
                    ↓
                  archived
**Status Definitions**:
- **pending**: Document uploaded, queued for processing
- **processing**: Document currently being processed (default for new uploads)
- **completed**: Processing finished successfully
- **failed**: Processing failed, manual intervention may be needed
- **archived**: Document inactive or soft-deleted
**Example Workflows**:
1. **Upload Flow**: Create document → Status: `processing` → Process → Update to `completed` or `failed`
2. **Reprocess Flow**: Update status from `failed` to `processing` → Process again
3. **Archive Flow**: Update status to `archived` for soft delete
4. **Query Flow**: Filter by status to see pending work, failures, or completed documents
### **Future Enhancements** (Not in scope):
- Document versioning
- Optional status transition validation (state machine for specific workflows)
- Status change audit log
- Automatic status transitions based on events
- Bulk status updates
- Bulk operations (upload/delete multiple)
- Document tagging system
- Full-text search in document names
- Document metadata extraction
- Document expiration/archival policies based on status
- Notifications on status changes
- Status value conventions or recommendations documentation
### **Security Considerations**:
- Blob paths should be validated to prevent path traversal
- Document hashes should use SHA-256 or stronger
- Status transitions should be validated (optional: implement state machine)
- Consider adding rate limiting for upload endpoints
- Implement virus scanning before setting status to `completed`
- Validate file types and sizes
- Log all status changes for audit purposhash
- `500 Internal Server Error` - Server error
---
## Notes
### **Design Decisions**:
1. **Hash Uniqueness**: Documents are identified uniquely by their hash to prevent duplicate uploads
2. **Soft Delete**: Consider implementing soft delete (IsDeleted flag) instead of hard delete for audit purposes
3. **Immutable Fields**: clientId and documentHash are not updatable after creation
4. **Administrator-Only Mutations**: Only admins can update/delete to prevent accidental data loss
5. **Pagination**: Default limit of 250 matches other APIs in the system
### **Future Enhancements** (Not in scope):
- Document versioning
- Bulk operations (upload/delete multiple)
- Document tagging system
- Full-text search in document names
- Document metadata extraction
- Audit trail for all operations
- Soft delete with restoration capability
- Document expiration/archival policies
### **Security Considerations**:
- Blob paths should be validated to prevent path traversal
- Document hashes should use SHA-256 or stronger
- Consider adding rate limiting for upload endpoints
- Implement virus scanning before document creation
- Validate file types and sizes