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

# Invoice Document Upload API

**# Design Story: Invoice Document Upload API**

**## Overview**

Implement a POST `/invoice_document/upload` endpoint in Node.js using Fastify that accepts invoice document uploads and forwards them to an external Fluxx API for processing.

**## User Story**

****As a**** user of the web-documents-ai application

****I want to**** upload invoice documents through a secure API endpoint

****So that**** the documents can be processed by the external Fluxx system for further analysis and workflow management

**## Technical Requirements**

**### API Specification**

* ****Method****: POST

* ****Endpoint****: `/invoice_document/upload/:client_id`

* ****Framework****: Node.js with Fastify

* ****Content-Type****: `multipart/form-data`

* ****Authentication****: JWT token validation required

* ****Authorization****: Role-based access control

**### External API Integration**

* ****Target****: Fluxx API upload endpoint

* ****Pattern****: `POST /{basePath}/upload/:client_id`

* ****Features****:

\- Multi-file upload support

\- Single file fallback

\- File validation and processing

**## Architecture Design**

**### 1. API Layer Structure**

```typescript

// Route Definition

interface UploadRouteParams {

  account_id: string;

}

  

interface UploadRequestBody {

  files: MultipartFile[];

  metadata?: {

    invoiceNumber?: string;

    vendorId?: string;

    clientId?: string;

    description?: string;

  };

}

  

interface UploadResponse {

  success: boolean;

  uploadId: string;

  clientId: string;

  files: UploadedFileInfo[];

  fluxxResponse?: any;

  errors?: string[];

}

```

**### 2. Component Architecture**

```

┌─────────────────────────────────────────────────────────────┐

│                    Client Application                        │

│                 (web-documents-ai)                          │

└──────────────────────┬──────────────────────────────────────┘

                       │ POST /invoice_document/upload/:appId

                       │ multipart/form-data

                       ▼

┌─────────────────────────────────────────────────────────────┐

│                  Fastify Server                             │

│  ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │

│  │   Auth Guard    │ │  Upload Route   │ │  Validation    │ │

│  │   Middleware    │ │    Handler      │ │   Middleware   │ │

│  └─────────────────┘ └─────────────────┘ └────────────────┘ │

│           │                   │                   │          │

│           ▼                   ▼                   ▼          │

│  ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │

│  │ Upload Service  │ │ Fluxx Service   │ │  File Service  │ │

│  │   Controller    │ │   Integration   │ │   Utilities    │ │

│  └─────────────────┘ └─────────────────┘ └────────────────┘ │

└──────────────────────┬──────────────────────────────────────┘

                       │ POST /{basePath}/upload/:appId

                       │ multipart/form-data

                       ▼

┌─────────────────────────────────────────────────────────────┐

│                   External Fluxx API                        │

│                 Upload Controller                           │

└─────────────────────────────────────────────────────────────┘

```

**## Implementation Plan**

**### Phase 1: Core Infrastructure**

**#### 1.1 Route Registration**

```typescript

// routes/invoiceUpload.ts

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

import { InvoiceUploadController } from '../controllers/invoiceUploadController';

import { authGuard } from '../middleware/authGuard';

import { roleGuard } from '../middleware/roleGuard';

  

export async function invoiceUploadRoutes(fastify: FastifyInstance) {

  // Register file upload support

  await fastify.register(require('@fastify/multipart'), {

    limits: {

      fileSize: 50 * 1024 * 1024, // 50MB max file size

      files: 10, // Maximum 10 files per request

    },

  });

  

  fastify.route({

    method: 'POST',

    url: '/invoice_document/upload/:client_id',

    preHandler: [

      authGuard, // JWT validation

      roleGuard(['admin', 'manager', 'user']), // Role-based access

    ],

    schema: {

      params: {

        type: 'object',

        properties: {

          client_id: { type: 'string', pattern: '^[a-zA-Z0-9-_]+$' }

        },

        required: ['client_id']

      },

      response: {

        200: {

          type: 'object',

          properties: {

            success: { type: 'boolean' },

            uploadId: { type: 'string' },

            client_id: { type: 'string' },

            files: {

              type: 'array',

              items: {

                type: 'object',

                properties: {

                  filename: { type: 'string' },

                  size: { type: 'number' },

                  contentType: { type: 'string' },

                  uploadedAt: { type: 'string' }

                }

              }

            }

          }

        },

        400: {

          type: 'object',

          properties: {

            error: { type: 'string' },

            details: { type: 'array', items: { type: 'string' } }

          }

        }

      }

    },

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

      const controller = new InvoiceUploadController();

      return controller.handleUpload(request, reply);

    },

  });

}

```

**#### 1.2 Upload Controller**

```typescript

// controllers/invoiceUploadController.ts

import { FastifyRequest, FastifyReply } from 'fastify';

import { FluxxService } from '../services/fluxxService';

import { FileValidationService } from '../services/fileValidationService';

import { AuditService } from '../services/auditService';

  

export class InvoiceUploadController {

  private fluxxService: FluxxService;

  private fileValidator: FileValidationService;

  private auditService: AuditService;

  

  constructor() {

    this.fluxxService = new FluxxService();

    this.fileValidator = new FileValidationService();

    this.auditService = new AuditService();

  }

  

  async handleUpload(request: FastifyRequest, reply: FastifyReply) {

    try {

      const { client_id } = request.params as { client_id: string };

      const userInfo = request.user; // From JWT middleware

  

      // Log upload attempt

      await this.auditService.logUploadAttempt(client_id, userInfo);

  

      // Check if multiple files or single file

      const filesIterator = request.files();

      const first = await filesIterator.next();

  

      if (!first.done) {

        // Multiple files upload

        return await this.handleMultipleFiles(client_id, first, filesIterator, userInfo);

      } else {

        // Single file upload

        return await this.handleSingleFile(request, client_id, userInfo);

      }

    } catch (error) {

      request.log.error('Upload error:', error);

      return reply.status(500).send({

        success: false,

        error: 'Internal server error during upload',

        details: process.env.NODE_ENV === 'development' ? [error.message] : []

      });

    }

  }

  

  private async handleMultipleFiles(

    client_id: string,

    first: any,

    filesIterator: any,

    userInfo: any

  ) {

    const validatedFiles = [];

  

    // Recreate file iterator including first file

    async function* allFiles() {

      yield first.value;

      for await (const file of filesIterator) {

        yield file;

      }

    }

  

    // Validate all files

    for await (const file of allFiles()) {

      const validation = await this.fileValidator.validateInvoiceFile(file);

      if (!validation.isValid) {

        throw new Error(`Invalid file ${file.filename}: ${validation.errors.join(', ')}`);

      }

      validatedFiles.push(file);

    }

  

    // Forward to Fluxx API

    const fluxxResponse = await this.fluxxService.uploadMultipleFiles(

      client_id,

      validatedFiles,

      userInfo

    );

  

    // Log successful upload

    await this.auditService.logUploadSuccess(client_id, validatedFiles, userInfo);

  

    return {

      success: true,

      uploadId: fluxxResponse.uploadId,

      client_id,

      files: validatedFiles.map(f => ({

        filename: f.filename,

        size: f.file.bytesRead,

        contentType: f.mimetype,

        uploadedAt: new Date().toISOString()

      })),

      fluxxResponse: fluxxResponse.data

    };

  }

  

  private async handleSingleFile(request: FastifyRequest, client_id: string, userInfo: any) {

    const data = await request.file();

  

    if (!data) {

      throw new Error('No file provided');

    }

  

    // Validate file

    const validation = await this.fileValidator.validateInvoiceFile(data);

    if (!validation.isValid) {

      throw new Error(`Invalid file: ${validation.errors.join(', ')}`);

    }

  

    const buffer = await data.toBuffer();

    const filename = data.filename ?? '';

    const contentType = data.mimetype ?? 'application/octet-stream';

  

    // Forward to Fluxx API

    const fluxxResponse = await this.fluxxService.uploadSingleFile(

      client_id,

      buffer,

      filename,

      contentType,

      userInfo

    );

  

    // Log successful upload

    await this.auditService.logUploadSuccess(client_id, [data], userInfo);

  

    return {

      success: true,

      uploadId: fluxxResponse.uploadId,

      client_id,

      files: [{

        filename,

        size: buffer.length,

        contentType,

        uploadedAt: new Date().toISOString()

      }],

      fluxxResponse: fluxxResponse.data

    };

  }

}

```

**### Phase 2: External API Integration**

**#### 2.1 Fluxx Service**

```typescript

// services/fluxxService.ts

import axios, { AxiosInstance } from 'axios';

import FormData from 'form-data';

  

export class FluxxService {

  private client: AxiosInstance;

  private basePath: string;

  

  constructor() {

    this.basePath = process.env.FLUXX_API_BASE_PATH || '/api/v1';

    this.client = axios.create({

      baseURL: process.env.FLUXX_API_URL,

      timeout: 300000, // 5 minutes for file uploads

      headers: {

        'Authorization': `Bearer ${process.env.FLUXX_API_TOKEN}`,

      },

    });

  }

  

  async uploadMultipleFiles(

    client_id: string,

    files: any[],

    userInfo: any

  ): Promise<any> {

    const formData = new FormData();

  

    // Add files to form data

    for (const file of files) {

      const buffer = await file.toBuffer();

      formData.append('files', buffer, {

        filename: file.filename,

        contentType: file.mimetype,

      });

    }

  

    // Add metadata

    formData.append('metadata', JSON.stringify({

      uploadedBy: userInfo.email,

      clientId: userInfo.selectedClient,

      timestamp: new Date().toISOString(),

    }));

  

    try {

      const response = await this.client.post(

        `${this.basePath}/upload/${client_id}`,

        formData,

        {

          headers: {

            ...formData.getHeaders(),

            'X-Uploaded-By': userInfo.email,

            'X-Client-Id': userInfo.selectedClient,

          },

        }

      );

  

      return {

        uploadId: response.data.uploadId || `upload-${Date.now()}`,

        data: response.data,

        status: response.status,

      };

    } catch (error) {

      throw new Error(`Fluxx API error: ${error.response?.data?.message || error.message}`);

    }

  }

  

  async uploadSingleFile(

    client_id: string,

    buffer: Buffer,

    filename: string,

    contentType: string,

    userInfo: any

  ): Promise<any> {

    const formData = new FormData();

    formData.append('file', buffer, {

      filename,

      contentType,

    });

  

    // Add metadata

    formData.append('metadata', JSON.stringify({

      uploadedBy: userInfo.email,

      clientId: userInfo.selectedClient,

      timestamp: new Date().toISOString(),

    }));

  

    try {

      const response = await this.client.post(

        `${this.basePath}/upload/${client_id}`,

        formData,

        {

          headers: {

            ...formData.getHeaders(),

            'X-Uploaded-By': userInfo.email,

            'X-Client-Id': userInfo.selectedClient,

          },

        }

      );

  

      return {

        uploadId: response.data.uploadId || `upload-${Date.now()}`,

        data: response.data,

        status: response.status,

      };

    } catch (error) {

      throw new Error(`Fluxx API error: ${error.response?.data?.message || error.message}`);

    }

  }

}

```

**### Phase 3: Validation and Security**

**#### 3.1 File Validation Service**

```typescript

// services/fileValidationService.ts

export interface FileValidationResult {

  isValid: boolean;

  errors: string[];

  warnings: string[];

}

  

export class FileValidationService {

  private allowedMimeTypes = [

    'application/pdf',

    'image/jpeg',

    'image/png',

    'image/tiff',

    'application/vnd.ms-excel',

    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',

  ];

  

  private maxFileSize = 50 * 1024 * 1024; // 50MB

  

  async validateInvoiceFile(file: any): Promise<FileValidationResult> {

    const errors: string[] = [];

    const warnings: string[] = [];

  

    // Check file size

    if (file.file.bytesRead > this.maxFileSize) {

      errors.push(`File size exceeds maximum limit of ${this.maxFileSize / 1024 / 1024}MB`);

    }

  

    // Check MIME type

    if (!this.allowedMimeTypes.includes(file.mimetype)) {

      errors.push(`File type ${file.mimetype} is not allowed for invoice uploads`);

    }

  

    // Check filename

    if (!file.filename || file.filename.length === 0) {

      errors.push('Filename is required');

    }

  

    // Check for suspicious file extensions

    const suspiciousExtensions = ['.exe', '.bat', '.cmd', '.com', '.scr', '.pif'];

    const extension = file.filename?.toLowerCase().split('.').pop();

    if (extension && suspiciousExtensions.includes(`.${extension}`)) {

      errors.push(`File extension .${extension} is not allowed`);

    }

  

    // Additional validation for PDF files

    if (file.mimetype === 'application/pdf') {

      try {

        const buffer = await file.toBuffer();

        if (!this.isPDFValid(buffer)) {

          errors.push('PDF file appears to be corrupted or invalid');

        }

      } catch (error) {

        warnings.push('Could not validate PDF file integrity');

      }

    }

  

    return {

      isValid: errors.length === 0,

      errors,

      warnings,

    };

  }

  

  private isPDFValid(buffer: Buffer): boolean {

    // Basic PDF header validation

    const pdfHeader = buffer.slice(0, 4).toString();

    return pdfHeader === '%PDF';

  }

}

```

**#### 3.2 Authentication Middleware**

```typescript

// middleware/authGuard.ts

import { FastifyRequest, FastifyReply } from 'fastify';

import jwt from 'jsonwebtoken';

  

export async function authGuard(request: FastifyRequest, reply: FastifyReply) {

  try {

    const authHeader = request.headers.authorization;

  

    if (!authHeader || !authHeader.startsWith('Bearer ')) {

      return reply.status(401).send({

        error: 'Authentication required',

        details: ['Missing or invalid authorization header']

      });

    }

  

    const token = authHeader.substring(7);

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

  

    // Attach user info to request

    request.user = {

      id: decoded.sub,

      email: decoded.email,

      roles: decoded.roles || [],

      selectedClient: decoded.selectedClient,

    };

  

  } catch (error) {

    return reply.status(401).send({

      error: 'Invalid token',

      details: ['Token verification failed']

    });

  }

}

```

**#### 3.3 Role-Based Access Control**

```typescript

// middleware/roleGuard.ts

import { FastifyRequest, FastifyReply } from 'fastify';

  

export function roleGuard(allowedRoles: string[]) {

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

    const user = request.user as any;

  

    if (!user) {

      return reply.status(401).send({

        error: 'Authentication required',

        details: ['User not authenticated']

      });

    }

  

    const hasRequiredRole = allowedRoles.some(role =>

      user.roles.includes(role) || user.roles.includes('admin')

    );

  

    if (!hasRequiredRole) {

      return reply.status(403).send({

        error: 'Insufficient permissions',

        details: [`Required roles: ${allowedRoles.join(', ')}`]

      });

    }

  };

}

```

**### Phase 4: Monitoring and Logging**

**#### 4.1 Audit Service**

```typescript

// services/auditService.ts

export class AuditService {

  async logUploadAttempt(client_id: string, userInfo: any): Promise<void> {

    console.log(`Upload attempt - Application: ${client_id}, User: ${userInfo.email}`);

  

    // In production, log to database or monitoring service

    // await this.database.audit.create({

    //   action: 'UPLOAD_ATTEMPT',

    //   client_id,

    //   userId: userInfo.id,

    //   userEmail: userInfo.email,

    //   clientId: userInfo.selectedClient,

    //   timestamp: new Date(),

    // });

  }

  

  async logUploadSuccess(client_id: string, files: any[], userInfo: any): Promise<void> {

    console.log(`Upload success - Application: ${client_id}, Files: ${files.length}, User: ${userInfo.email}`);

  

    // In production, log to database or monitoring service

    // await this.database.audit.create({

    //   action: 'UPLOAD_SUCCESS',

    //   client_id,

    //   userId: userInfo.id,

    //   userEmail: userInfo.email,

    //   clientId: userInfo.selectedClient,

    //   fileCount: files.length,

    //   filenames: files.map(f => f.filename),

    //   timestamp: new Date(),

    // });

  }

}

```

**## Testing Strategy**

**### Unit Tests**

```typescript

// tests/controllers/invoiceUploadController.test.ts

import { InvoiceUploadController } from '../../src/controllers/invoiceUploadController';

import { createMockRequest, createMockReply } from '../helpers/mockFastify';

  

describe('InvoiceUploadController', () => {

  let controller: InvoiceUploadController;

  

  beforeEach(() => {

    controller = new InvoiceUploadController();

  });

  

  describe('handleUpload', () => {

    it('should handle single file upload successfully', async () => {

      const mockRequest = createMockRequest({

        params: { client_id: 'test-app-123' },

        user: { email: 'test@example.com', roles: ['user'] },

        file: () => Promise.resolve({

          filename: 'invoice.pdf',

          mimetype: 'application/pdf',

          toBuffer: () => Buffer.from('mock pdf content'),

        }),

      });

  

      const mockReply = createMockReply();

  

      const result = await controller.handleUpload(mockRequest, mockReply);

  

      expect(result.success).toBe(true);

      expect(result.client_id).toBe('test-app-123');

      expect(result.files).toHaveLength(1);

    });

  

    it('should handle validation errors', async () => {

      const mockRequest = createMockRequest({

        params: { client_id: 'test-app-123' },

        user: { email: 'test@example.com', roles: ['user'] },

        file: () => Promise.resolve({

          filename: 'malicious.exe',

          mimetype: 'application/x-executable',

          toBuffer: () => Buffer.from('malicious content'),

        }),

      });

  

      const mockReply = createMockReply();

  

      await expect(controller.handleUpload(mockRequest, mockReply))

        .rejects.toThrow('File type application/x-executable is not allowed');

    });

  });

});

```

**### Integration Tests**

```typescript

// tests/integration/invoiceUpload.test.ts

import { build } from '../helpers/app';

import FormData from 'form-data';

  

describe('Invoice Upload API', () => {

  const app = build();

  

  afterAll(async () => {

    await app.close();

  });

  

  it('should upload invoice document successfully', async () => {

    const form = new FormData();

    form.append('file', Buffer.from('mock pdf'), {

      filename: 'test-invoice.pdf',

      contentType: 'application/pdf',

    });

  

    const response = await app.inject({

      method: 'POST',

      url: '/invoice_document/upload/test-app-123',

      headers: {

        authorization: 'Bearer valid-jwt-token',

        ...form.getHeaders(),

      },

      payload: form,

    });

  

    expect(response.statusCode).toBe(200);

    const result = JSON.parse(response.payload);

    expect(result.success).toBe(true);

    expect(result.client_id).toBe('test-app-123');

  });

  

  it('should reject unauthorized requests', async () => {

    const response = await app.inject({

      method: 'POST',

      url: '/invoice_document/upload/test-app-123',

      payload: {},

    });

  

    expect(response.statusCode).toBe(401);

  });

});

```

**## Environment Configuration**

**### Required Environment Variables**

```bash

# .env

FLUXX_API_URL=[https://api.fluxx.example.com](https://api.fluxx.example.com "https://api.fluxx.example.com/")

FLUXX_API_BASE_PATH=/api/v1

FLUXX_API_TOKEN=your-fluxx-api-token

JWT_SECRET=your-jwt-secret

NODE_ENV=development

  

# File upload limits

MAX_FILE_SIZE=52428800  # 50MB in bytes

MAX_FILES_PER_REQUEST=10

  

# Logging

LOG_LEVEL=info

```

**## Deployment Considerations**

**### Production Checklist**

* [ ] Configure proper file size limits

* [ ] Set up monitoring and alerting

* [ ] Implement rate limiting

* [ ] Configure CORS policies

* [ ] Set up SSL/TLS termination

* [ ] Configure backup and disaster recovery

* [ ] Implement database auditing

* [ ] Set up log aggregation

**### Monitoring**

* API response times

* File upload success/failure rates

* External API (Fluxx) availability

* File validation error rates

* Authentication/authorization failures

**## API Documentation**

**### OpenAPI Schema**

```yaml

openapi: 3.0.0

info:

  title: Invoice Document Upload API

  version: 1.0.0

paths:

  /invoice_document/upload/{client_id}:

    post:

      summary: Upload invoice documents

      security:

        - bearerAuth: []

      parameters:

        - name: client_id

          in: path

          required: true

          schema:

            type: string

            pattern: '^[a-zA-Z0-9-_]+$'

      requestBody:

        content:

          multipart/form-data:

            schema:

              type: object

              properties:

                files:

                  type: array

                  items:

                    type: string

                    format: binary

                metadata:

                  type: object

                  properties:

                    invoiceNumber:

                      type: string

                    vendorId:

                      type: string

      responses:

        '200':

          description: Upload successful

          content:

            application/json:

              schema:

                type: object

                properties:

                  success:

                    type: boolean

                  uploadId:

                    type: string

                  client_id:

                    type: string

                  files:

                    type: array

                    items:

                      type: object

```

---