> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://cla-demo.postmansolutions.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://cla-demo.postmansolutions.com/_mcp/server.

# 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:

```sql

-- 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**:

```sql

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`):

```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):

```sql

-- 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):

```properties

# 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**:

```bash

# Validate migration syntax  

flyway validate  

  

# Check migration status  

flyway info  

  

# Run the migration  

flyway migrate  

  

# Rollback (if using undo migrations)  

flyway undo  

```

**Verification Query**:

```sql

-- 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**:

```typescript

// 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**:

```typescript

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**:

```sql

-- 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 ...;  

```

2. **Get Documents with Pagination and Filters**:

```sql

-- 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;  

```

3. **Update Document** (including status):

```sql

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;  

```

4. **Update 4ocument Status** (specific method):

```sql

UPDATE Documents  

SET Status = @status  

WHERE DocumentId = @documentId;  

```

5. **Delete Document** (consider soft delete using status):

```sql

-- 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;  

```

6. **Create Document** (ensure status is set):

```sql

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**:

```typescript

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**:

```typescript

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**:

```typescript

const documentSchema = {  

  getDocuments: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    querystring: {  

      type: 'object',  

      properties: {  

        clientId: {  

          type: 'integer',  

          description: 'Filter documents by client ID',  

        },  

        name: {  

          type: 'string',  

          description: 'Filter documents by name (partial match)',  

        },  

        status: {  

          type: 'string',  

          description: 'Filter documents by status (arbitrary string value)',  

        },  

        dateFrom: {  

          type: 'string',  

          format: 'date-time',  

          description: 'Filter documents uploaded from this date (ISO 8601)',  

        },  

        dateTo: {  

          type: 'string',  

          format: 'date-time',  

          description: 'Filter documents uploaded until this date (ISO 8601)',  

        },  

        limit: {  

          type: 'integer',  

          minimum: 1,  

          maximum: 250,  

          default: 250,  

          description: 'Number of items to return per page (max 250)',  

        },  

        offset: {  

          type: 'integer',  

          minimum: 0,  

          default: 0,  

          description: 'Number of items to skip',  

        },  

      },  

    },  

    response: {  

      200: {  

        type: 'object',  

        properties: {  

          total: {  

            type: 'integer',  

            description: 'Total number of documents matching filters',  

          },  

          limit: { type: 'integer' },  

          offset: { type: 'integer' },  

          items: {  

            type: 'array',  

            items: {  

              type: 'object',  

              properties: {  

                documentId: { type: 'integer' },  

                blobPath: { type: 'string' },  

                clientId: { type: 'integer' },  

                dateUploaded: { type: 'string', format: 'date-time' },  

                documentHash: { type: 'string' },  

                name: { type: 'string' },  

                status: {  

                  type: 'string',  

                  description: 'Current status of the document (arbitrary string)'  

                },  

              },  

            },  

          },  

        },  

      },  

      401: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      500: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

    },  

  },  

  

  getDocumentById: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    params: {  

      type: 'object',  

      required: ['documentId'],  

      properties: {  

        documentId: {  

          type: 'integer',  

          description: 'The unique identifier of the document',  

        },  

      },  

    },  

    response: {  

      20  status: {  

            type: 'string',  

            description: 'Current status of the document (arbitrary string)'  

          },  

        0: {  

        type: 'object',  

        properties: {  

          documentId: { type: 'integer' },  

          blobPath: { type: 'string' },  

          clientId: { type: 'integer' },  

          dateUploaded: { type: 'string', format: 'date-time' },  

          documentHash: { type: 'string' },  

          name: { type: 'string' },  

        },  

      },  

      404: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      401: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      500: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

    },  

  },  

  

  getDocumentsByClientId: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    params: {  

      type: 'object',  

      required: ['clientId'],  

      properties: {  

        clientId: {  

          type: 'integer',  

          description: 'The client ID to retrieve documents for',  

        },  

      },  

    },  

    querystring: {  

      type: 'object',  

      properties: {  

        limit: {  

          type: 'integer',  

          minimum: 1,  

          maximum: 250,  

          default: 250,  

        },  

        offset: {  

          type: 'integer',  

          minimum: 0,  

          default: 0,  

        },  

      },  

    },  

    response: {  

      200: {  

        type: 'object',  

        properties: {  

          count: { type: 'integer' },  

          limi  status: {  

                  type: 'string',  

                  description: 'Current status of the document (arbitrary string)'  

                },  

              t: { type: 'integer' },  

          offset: { type: 'integer' },  

          items: {  

            type: 'array',  

            items: {  

              type: 'object',  

              properties: {  

                documentId: { type: 'integer' },  

                blobPath: { type: 'string' },  

                clientId: { type: 'integer' },  

                dateUploaded: { type: 'string', format: 'date-time' },  

                documentHash: { type: 'string' },  

                name: { type: 'string' },  

              },  

            },  

          },  

        },  

      },  

      401: { type: 'object', properties: { error: { type: 'string' } } },  

      500: { type: 'object', properties: { error: { type: 'string' } } },  

    },  

  },  

  

  createDocument: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    body: {  

      type: 'object',  

      required: ['name', 'blobPath', 'clientId', 'documentHash'],  

      additionalProperties: false,  

      properties: {  

        name: {  

          type: 'string',  

          minLength: 1,  

          maxLength: 255,  

          description: 'The name of the document',  

        },  

        status: {  

          type: 'string',  

          default: 'processing',  

          description: 'Initial status of the document (defaults to processing, accepts any string)',  

        },  

      },  

    },  

    response: {  

      201: {  

        type: 'object',  

        properties: {  

          documentId: { type: 'integer' },  

          blobPath: { type: 'string' },  

          clientId: { type: 'integer' },  

          dateUploaded: { type: 'string', format: 'date-time' },  

          documentHash: { type: 'string' },  

          name: { type: 'string' },  

          status: {  

            type: 'string',  

            description: 'Current status of the document (arbitrary string)'  

           

          type: 'string',  

          minLength: 1,  

          maxLength: 100,  

          description: 'SHA256 hash of the document for duplicate detection',  

        },  

      },  

    },  

    response: {  

      201: {  

        type: 'object',  

        properties: {  

          documentId: { type: 'integer' },  

          blobPath: { type: 'string' },  

          clientId: { type: 'integer' },  

          dateUploaded: { type: 'string', format: 'date-time' },  

          documentHash: { type: 'string' },  

          name: { type: 'string' },  

        },  

      },  

      400: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      401: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      409: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

        description: 'Document with this hash already exists',  

      },  

      500: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

    },  

  },  

  

  updateDocument: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    pa  status: {  

          type: 'string',  

          description: 'Current status of the document (arbitrary string)',  

          description: 'Updated document status',  

        },  

      rams: {  

      type: 'object',  

      required: ['documentId'],  

      properties: {  

        documentId: {  

          type: 'integer',  

          description: 'The unique identifier of the document to update',  

        },  

      },  

    },  

    body: {  

      type: 'object',  

      additionalProperties: false,  

      minProperties: 1,  

      properties: {  

        name: {  

          type: 'string',  

          minLength: 1,  

          maxLength: 255,  

          description: 'Updated document name',  

        },  

        blobPath: {  

          type: 'string',  

          minLength: 1,  

          maxLength: 500,  

          description: 'Updated blob storage path',  

        },  

      },  

    },  

    response: {  

      204: {  

        description: 'Document updated successfully',  

        type: 'null',  

      },  

      400: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      401: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      404: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

      500: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

        },  

      },  

    },  

  },  

  

  deleteDocument: {  

    security: [{ bearerAuth: [] }],  

    tags: ['Documents'],  

    params: {  

      type: 'object',  

      required: ['documentId'],  

      properties: {  

        documentId: {  

          type: 'integer',  

          description: 'The unique identifier of the document to delete',  

        },  

      },  

    },  

    response: {  

      204: {  

        description: 'Document deleted successfully',  

        type: 'null',  

      },  

      401: {  

        type: 'object',  

        properties: {  

          error: { type: 'string' },  

    Status field included in all request/response schemas  

- ✅ Status enum values defined consistently across all schemas  

- ✅ Status defaults to 'processing' for new documents  

- ✅ Status is optional in update operations  

- ✅ Status filter included in GET /documents querystring  

- ✅ 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 2 (Types)  

  

**Estimated Effort**: 2-3 hours  

  

---  

  

### **Story 7  

  },  

};  

  

export default documentSchema;  

```

**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**:

```typescript

import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';  

import ROLES from '../../../auth/roles.js';  

import SCOPES from '../../../auth/scopes.js';  

import {  

          status?: string;  

          dateFrom?: string;  

          dateTo?: string;  

        };  

  

        const limit = Math.min(  

          query.limit || DEFAULT_QUERY_LIMIT,  

          DEFAULT_QUERY_LIMIT  

        );  

        const offset = Math.max(  

          query.offset || DEFAULT_QUERY_OFFSET,  

          DEFAULT_QUERY_OFFSET  

        );  

  

        const pagination: PaginationParams = { limit, offset };  

        const filters: DocumentFilters = {};  

  

        if (query.clientId !== undefined) filters.clientId = query.clientId;  

        if (query.name !== undefined && query.name.trim()) {  

          filters.name = query.name.trim();  

        }  

        if (query.status !== undefined && query.status.trim()) {  

          filters.status = query.status.trim();  

          limit?: number;  

          offset?: number;  

          clientId?: number;  

          name?: string;  

          dateFrom?: string;  

          dateTo?: string;  

        };  

  

        const limit = Math.min(  

          query.limit || DEFAULT_QUERY_LIMIT,  

          DEFAULT_QUERY_LIMIT  

        );  

        const offset = Math.max(  

          query.offset || DEFAULT_QUERY_OFFSET,  

          DEFAULT_QUERY_OFFSET  

        );  

  

        const pagination: PaginationParams = { limit, offset };  

        const filters: DocumentFilters = {};  

  

        if (query.clientId !== undefined) filters.clientId = query.clientId;  

        if (query.name !== undefined && query.name.trim()) {  

          filters.name = query.name.trim();  

        }  

        if (query.dateFrom !== undefined) filters.dateFrom = query.dateFrom;  

        if (query.dateTo !== undefined) filters.dateTo = query.dateTo;  

  

        const response = await DocumentController.getDocuments(  

          pagination,  

          filters  

        );  

        reply.status(200).send(response);  

      } catch (err) {  

        request.log.error(err);  

        reply.status(500).send({ error: 'Internal Server Error' });  

      }  

    },  

  });  

  

  // GET /api/v1/documents/:documentId - Get document by ID  

  fastify.get(`${basePath}/documents/:documentId`, {  

    schema: documentSchema.getDocumentById,  

    preValidation: [fastify.authenticate, fastify.authorize],  

    config: {  

      validRoles: [ROLES.User, ROLES.Administrator],  

      validScopes: [SCOPES.DOCUMENTS_READ],  

    },  

    handler: async (request: FastifyRequest, reply: FastifyReply) => {  

      try {  

        const { documentId } = request.params as { documentId: number };  

        const document = await DocumentController.getDocumentById(documentId);  

        reply.status(200).send(document);  

      } catch (err: unknown) {  

        request.log.error(err);  

        const message = getErrorMessage(err);  

        if (message === 'Document not found') {  

          return reply.status(404).send({ error: 'Document not found' });  

        }  

        reply.status(500).send({ error: 'Internal Server Error' });  

      }  

    },  

  });  

  

  // GET /api/v1/clients/:clientId/documents - Get documents by client ID  

  fastify.get(`${basePath}/clients/:clientId/documents`, {  

    schema: documentSchema.getDocumentsByClientId,  

    preValidation: [fastify.authenticate, fastify.authorize],  

    config: {  

      validRoles: [ROLES.User, ROLES.Administrator],  

      validScopes: [SCOPES.DOCUMENTS_READ],  

    },  

    handler: async (request: FastifyRequest, reply: FastifyReply) => {  

      try {  

        const { clientId } = request.params as { clientId: number };  

        const query = request.query as {  

          limit?: number;  

          offset?: number;  

        };  

  

        const limit = Math.min(  

          query.limit || DEFAULT_QUERY_LIMIT,  

          DEFAULT_QUERY_LIMIT  

        );  

        const offset = Math.max(  

          query.offset || DEFAULT_QUERY_OFFSET,  

          DEFAULT_QUERY_OFFSET  

        );  

  

        const pagination: PaginationParams = { limit, offset };  

        const response = await DocumentController.getDocumentsByClientId(  

          clientId,  

          pagination  

        );  

        reply.status(200).send(response);  

      } catch (err) {  

        request.log.error(err);  

        reply.status(500).send({ error: 'Internal Server Error' });  

      }  

    },  

  });  

  

  // POST /api/v1/documents - Create new document  

  fastify.post(`${basePath}/documents`, {  

    schema: documentSchema.createDocument,  

    preValidation: [fastify.authenticate, fastify.authorize],  

    config: {  

      validRoles: [ROLES.User, ROLES.Administrator],  

      validScopes: [SCOPES.DOCUMENTS_WRITE],  

    },  

    handler: async (request: FastifyRequest, reply: FastifyReply) => {  

      try {  

        const newDocument = request.body as NewDocument;  

        const document = await DocumentController.createDocument(newDocument);  

        reply.status(201).send(document);  

      } catch (err: unknown) {  

        request.log.error(err);  

        const message = getErrorMessage(err);  

         

        // Check for conflict error (duplicate hash)  

        if (message.includes('hash already exists')) {  

          return reply.status(409).send({  

            error: 'A document with this hash already exists',  

          });  

        }  

         

        reply.status(500).send({ error: 'Internal Server Error' });  

      }  

    },  

  });  

  

  // PATCH /api/v1/documents/:documentId - Update document  

  fastify.patch(`${basePath}/documents/:documentId`, {  

    schema: documentSchema.updateDocument,  

    preValidation: [fastify.authenticate, fastify.authorize],  

    config: {  

      validRoles: [ROLES.Administrator],  

      validScopes: [SCOPES.DOCUMENTS_WRITE],  

    },  

    handler: async (request: FastifyRequest, reply: FastifyReply) => {  

      try {  

        const { documentId } = request.params as { documentId: number };  

        const updateData = request.body as DocumentUpdateData;  

  

        await DocumentController.updateDocument(documentId, updateData);  

        reply.status(204).send();  

      } catch (err: unknown) {  

        request.log.error(err);  

        const message = getErrorMessage(err);  

         

        if (message === 'Document not found') {  

          return reply.status(404).send({ error: 'Document not found' });  

        }  

         

        if (message === 'No valid fields provided for update') {  

          return reply.status(400).send({  

            error: 'No valid fields provided for update',  

          });  

        }  

         

        reply.status(500).send({ error: 'Internal Server Error' });  

      }  

    },  

  });  

  

  // DELETE /api/v1/documents/:documentId - Delete document  

  fastify.delete(`${basePath}/documents/:documentId`, {  

    schema: documentSchema.deleteDocument,  

    preValidation: [fastify.authenticate, fastify.authorize],  

    config: {  

      validRoles: [ROLES.Administrator],  

      validScopes: [SCOPES.DOCUMENTS_WRITE],  

    },  

    handler: async (request: FastifyRequest, reply: FastifyReply) => {  

    Status field handling in all routes  

- ✅ Status filter support in GET /documents  

- ✅ Status updates supported in PATCH endpoint  

- ✅ Default status 'processing' for POST endpoint  

- ✅ Authentication middleware applied (`fastify.authenticate`)  

- ✅ Authorization middleware applied (`fastify.authorize`)  

- ✅ Proper role-based access control (RBAC)  

  - Read operations: User and Administrator roles  

  - Write operations: Administrator role only  

- ✅ Scope-based authorization  

  - Read: `SCOPES.DOCUMENTS_READ`  

  - Write: `SCOPES.DOCUMENTS_WRITE`  

- ✅ HTTP status codes follow REST standards:  

  - 200 for successful GET  

  - 201 for successful POST  

  - 204 for successful PATCH/DELETE  

  - 400 for bad request  

  - 401 for unauthorized  

  - 404 for not found  

  - 409 for conflict (duplicate hash)  

  - 500 for server errors  

- ✅ Pagination support with limit/offset  

- ✅ Filter parameter handling (including status)  

- ✅ Error handling for all endpoints  

- ✅ Proper error logging  

- ✅ Type-safe request/response handling  

  

**Security Considerations**:  

- ✅ Only administrators can update/delete documents  

- ✅ Users can read documents they have access to  

- ✅ Status can be set to any string value (no validation)  

- ✅ Hash uniqueness enforced to prevent duplicates  

- ✅ Input validation via schemas  

  

**Dependencies**: Story 5 (Controller), Story 6 (Schema)  

  

**Estimated Effort**: 3-4 hours  

  

---  

  

### **Story 8ad request  

  - 401 for unauthorized  

  - 404 for not found  

  - 409 for conflict (duplicate hash)  

  - 500 for server errors  

- ✅ Pagination support with limit/offset  

- ✅ Filter parameter handling  

- ✅ Error handling for all endpoints  

- ✅ Proper error logging  

- ✅ Type-safe request/response handling  

  

**Security Considerations**:  

- ✅ Only administrators can update/delete documents  

- ✅ Users can read documents they have access to  

- ✅ Hash uniqueness enforced to prevent duplicates  

- ✅ Input validation via schemas  

  

**Dependencies**: Story 4 (Controller), Story 5 (Schema)  

  

**Estimated Effort**: 3-4 hours  

  

---  

  

### **Story 7: Register Routes in Main Application** (1 point)  

**Priority**: 🔥 **Critical**    

**Status**: ⚠️ **Needs Implementation**  

  

**Description**: Register the document routes with the main Fastify application.  

  

**Implementation Location**: `src/routes/index.ts`  

7 (Routes)  

  

**Estimated Effort**: 15 minutes  

  

---  

  

### **Story 9outes (add alongside existing route registrations)  

export default async function routes(fastify: FastifyInstance) {  

  // ... existing routes ...  

   

  // Register document routes  

  fastify.register(documentRoutesV1);  

   

  // ... other routes ...  

}  

```

**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**

7.

**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**:  

  

```

```typescript

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**:  

  

```

```typescript

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**:  

  

```

```typescript

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

4. Story 3: Service Layer

5. Story 4: Controller Layer

6. Story 5: Schema Definitions

7. Story 6: Route Handlers

8. Story 7: Route Registration

9. Story 9: Model Tests

10. Story 10: Service Tests

11. Story 11: Integration Tests

12. 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

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

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

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

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

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

7. **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
```