Story 1.16.3: Generic File Upload Service
Table of Contents
Story 1.16.3: Generic File Upload Service
Status
Done
Story
As a developer, I want a generic file upload service that is independent of entity creation, so that files can be uploaded before entities exist, eliminating circular dependencies and enabling better user experience.
Domain Context
Primary Domain
Shared/Infrastructure (Cross-cutting file upload concerns)
Involved Services
- Company User Management Service - Immediate use: Company logo uploads
- User Service - Future use: Profile picture uploads
- Event Management Service - Future use: Event banner uploads
- Partner Coordination Service - Future use: Partner logo uploads
Shared-Kernel Foundation (automatically included):
- Exception hierarchy (NotFoundException, ValidationException, BusinessException)
- ErrorResponse (standardized error format with correlationId, path, severity)
- DomainEvent & DomainEventPublisher (event-driven architecture)
- Value objects (UUID identifiers)
- SecurityContextHelper (user context extraction)
Cross-Domain Dependencies
- AWS S3: Direct file storage with presigned URLs
- AWS CloudFront: CDN for file delivery
- Scheduled Job: Cleanup service for orphaned uploads
Requirements Context
Related Functional Requirements
- FR-CMP-01: Company profile management with logo support
- FR-USR-02: User profile with profile picture (future)
- FR-EVT-03: Event management with banner images (future)
Problem Statement
Current architecture has circular dependency when creating companies with logos:
- Logo upload endpoint requires company to exist:
POST /companies/{name}/logo/presigned-url - Company cannot be created without being created first
- Current workaround: Create company without logo, then edit to add logo (poor UX)
- Same problem will affect user profiles, event banners, etc.
Acceptance Criteria Source
Epic 1.16 (Company Management Foundation), new acceptance criteria for file upload architecture
Architecture Context
Architecture Patterns
Referenced patterns from architecture docs:
- Data Models (03-data-architecture.md): New logos table with lifecycle states
- API Design (04-api-design.md): RESTful presigned URL pattern, entity-agnostic endpoints
- Backend (06-backend-architecture.md): Spring Boot service with scheduled cleanup jobs
- Frontend (05-frontend-architecture.md): Reusable upload components with React hooks
Database Schema Reference
Architecture Schema (Source of Truth):
- Doc: docs/architecture/ADR-002-generic-file-upload-service.md
- New Table:
logoswith state machine (PENDING → CONFIRMED → ASSOCIATED)
Migration Check (Dev Agent completes in Task 0):
- Existing Migration: None (new feature)
- Status: ❌ Create new
- Action: Create V6__Create_logos_table.sql
Infrastructure Components
- S3 Bucket: batbern-{env}-company-logos
- Temp prefix:
logos/temp/{uploadId}/ - Final prefix:
logos/{year}/{entity-type}/{entity-name}/
- Temp prefix:
- CloudFront Distribution: CDN for logo delivery
- Scheduled Job: Daily cleanup of orphaned uploads
- PENDING status > 24 hours old
- CONFIRMED status > 7 days old (not associated)
Wireframe Context
UI Components
No specific wireframes (infrastructure component). Reusable across:
- Company Management screen (Story 2.5.1)
- User Profile screen (future)
- Event Management screen (future)
React Components
New Components:
-
FileUpload.tsx (
web-frontend/src/components/shared/FileUpload/)- Props:
{ entityType?, entityId?, currentFileUrl?, onUploadSuccess, onUploadError, maxSize, allowedTypes } - Material-UI: Box, Typography, LinearProgress, IconButton, Alert
- State: uploadProgress, isUploading, error, fileUrl
- Supports drag-and-drop via react-dropzone
- Props:
-
useFileUpload hook (
web-frontend/src/hooks/useFileUpload/)- Logic: Generate presigned URL → Upload to S3 → Confirm upload
- Returns:
{ uploadFile, progress, isUploading, error, reset }
Acceptance Criteria
-
AC1: Generic Upload Initiation
- Generic presigned URL endpoint
POST /api/v1/logos/presigned-urlworks without entity existence - Validates file type (PNG, JPEG, SVG) and size (max 5MB)
- Creates Logo entity with status PENDING
- Returns presigned S3 URL for temp location:
logos/temp/{uploadId}/
- Generic presigned URL endpoint
-
AC2: Upload Confirmation
- Confirmation endpoint
POST /api/v1/logos/{uploadId}/confirmupdates Logo status to CONFIRMED - Stores checksum for integrity verification
- Logo record persists independently of any entity
- Confirmation endpoint
-
AC3: Entity Association
- Entity creation can reference uploaded file via logoUploadId field
- Company creation with logoUploadId copies S3 file from temp to final location
- Final S3 key format:
logos/{year}/{entity-type}/{entity-name}/logo-{fileId}.{ext} - Logo status updated to ASSOCIATED with entity reference
-
AC4: S3 File Management
- S3 copy operation from temp to final location on association
- Original temp file cleaned up after successful copy
- CloudFront URL generated for final location
-
AC5: Automated Cleanup
- Scheduled job runs daily
- Deletes PENDING logos older than 24 hours
- Deletes CONFIRMED (not associated) logos older than 7 days
- Corresponding S3 files also deleted
-
AC6: Frontend Component Reusability
- FileUpload component supports generic mode (no entity) and entity-specific mode
- useFileUpload hook handles presigned URL flow
- Component location:
web-frontend/src/components/shared/FileUpload/(NOT under Company)
-
AC7: Legacy Endpoint Removal
- Old company-specific logo endpoints removed:
POST /companies/{name}/logo/presigned-url(DELETED)POST /companies/{name}/logo/confirm(DELETED)
- CompanyLogoService.java deleted entirely
- Old LogoUpload.tsx component removed from Company folder
- Old company-specific logo endpoints removed:
-
AC8: Test Coverage
- Unit tests for GenericLogoService (upload, confirm, associate, copy)
- Integration tests for LogoController endpoints
- Unit tests for LogoCleanupService
- Frontend tests for FileUpload component and useFileUpload hook
- E2E test: Create company with logo in single operation
Test Specifications (TDD)
Test Scenarios by Acceptance Criteria
AC1 Tests (Upload Initiation):
- Test 1.1: should_generatePresignedUrl_when_validFileProvided
- Test 1.2: should_createLogoWithPendingStatus_when_uploadRequested
- Test 1.3: should_throwFileSizeExceeded_when_fileSizeOver5MB
- Test 1.4: should_throwInvalidFileType_when_unsupportedExtension
- Test 1.5: should_generateUniqueTempS3Key_when_uploadRequested
AC2 Tests (Upload Confirmation):
- Test 2.1: should_updateLogoToConfirmed_when_uploadConfirmed
- Test 2.2: should_storeChecksum_when_confirmationProvided
- Test 2.3: should_throwNotFound_when_invalidUploadId
AC3 Tests (Entity Association):
- Test 3.1: should_associateLogoWithCompany_when_logoUploadIdProvided
- Test 3.2: should_ignoreLogoAssociation_when_logoUploadIdNull
- Test 3.3: should_updateLogoStatus_when_associationSuccessful
AC4 Tests (S3 File Management):
- Test 4.1: should_copyS3Object_when_associatingLogo
- Test 4.2: should_generateFinalS3Key_when_associating
- Test 4.3: should_generateCloudFrontUrl_when_associationComplete
- Test 4.4: should_deleteTempFile_when_copySuccessful
AC5 Tests (Cleanup):
- Test 5.1: should_deletePendingLogos_when_olderThan24Hours
- Test 5.2: should_deleteConfirmedLogos_when_olderThan7Days
- Test 5.3: should_preserveAssociatedLogos_when_cleanupRuns
- Test 5.4: should_deleteCorrespondingS3Files_when_cleaningUpLogos
AC6 Tests (Frontend):
- Test 6.1: should_uploadFile_when_dropzoneUsed
- Test 6.2: should_showProgress_when_uploadInProgress
- Test 6.3: should_callOnSuccess_when_uploadCompletes
- Test 6.4: should_callOnError_when_uploadFails
AC7 Tests (Cleanup):
- Test 7.1: should_return404_when_accessingOldLogoEndpoint
- Test 7.2: should_compileWithoutCompanyLogoService_when_deleted
AC8 Tests (Integration):
- Test 8.1: should_createCompanyWithLogo_when_logoUploadIdProvided_E2E
Test File Locations
Backend Tests:
- Unit:
services/company-user-management-service/src/test/java/ch/batbern/companyuser/service/GenericLogoServiceTest.java - Unit:
services/company-user-management-service/src/test/java/ch/batbern/companyuser/service/LogoCleanupServiceTest.java - Integration:
services/company-user-management-service/src/test/java/ch/batbern/companyuser/controller/LogoControllerTest.java - Integration:
services/company-user-management-service/src/test/java/ch/batbern/companyuser/integration/CompanyWithLogoIntegrationTest.java
Frontend Tests:
- Unit:
web-frontend/src/components/shared/FileUpload/__tests__/FileUpload.test.tsx - Unit:
web-frontend/src/hooks/useFileUpload/__tests__/useFileUpload.test.ts - Integration:
web-frontend/src/components/shared/Company/__tests__/CompanyForm.test.tsx(updated) - E2E:
web-frontend/e2e/workflows/company-management/company-creation.spec.ts(updated)
Test Data & Mocks
Test Data Builders:
- LogoTestDataBuilder for creating test Logo entities with various states
- CompanyTestDataBuilder enhancement for logoUploadId
Mock Services:
- Mock S3Presigner for presigned URL generation
- Mock S3Client for copy/delete operations
- Mock scheduled job execution for cleanup tests
Test Containers (MANDATORY):
- PostgreSQL 16 Alpine via Testcontainers for ALL integration tests
- All integration tests MUST extend AbstractIntegrationTest
- Use singleton pattern with withReuse(true) for performance
- Enable Flyway migrations for production parity
- LocalStack for AWS S3 mocking
Test Configuration:
- application-test.properties must use PostgreSQL dialect
- spring.jpa.hibernate.ddl-auto=validate (let Flyway manage schema)
- spring.flyway.enabled=true for testing real migrations
- Mock S3 with LocalStack or Testcontainers S3
Tasks / Subtasks (TDD Workflow)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Delete web-frontend/src/components/shared/Company/tests
-
-
-
Dev Notes - Implementation Guide
Technical Design Notes
State Machine:
PENDING (upload requested)
↓
CONFIRMED (file uploaded to S3, checksum verified)
↓
ASSOCIATED (linked to entity, copied to final location)
Service Architecture:
- GenericLogoService: Core upload/confirm/associate logic
- LogoCleanupService: Scheduled job (@Scheduled cron)
- LogoController: REST API endpoints
S3 Key Strategy:
- Temp:
logos/temp/{uploadId}/logo-{fileId}.{ext} - Final:
logos/{year}/{entity-type}/{entity-name}/logo-{fileId}.{ext}
Transaction Boundaries:
- Upload initiation: Single transaction (create Logo entity)
- Confirmation: Single transaction (update Logo status)
- Association: Transaction with company creation (atomic)
Error Handling:
- FileSizeExceededException (>5MB)
- InvalidFileTypeException (not PNG/JPEG/SVG)
- LogoNotFoundException (invalid uploadId)
- S3OperationException (copy/delete failures)
API Contracts
POST /api/v1/logos/presigned-url
requestBody:
content:
application/json:
schema:
type: object
required: [fileName, fileSize, mimeType]
properties:
fileName:
type: string
example: "company-logo.png"
fileSize:
type: integer
format: int64
maximum: 5242880 # 5MB
example: 102400
mimeType:
type: string
enum: [image/png, image/jpeg, image/svg+xml]
example: "image/png"
responses:
'200':
description: Presigned URL generated
content:
application/json:
schema:
type: object
properties:
uploadUrl:
type: string
format: uri
example: "https://s3.amazonaws.com/bucket/key?signature=..."
uploadId:
type: string
description: "Internal upload identifier"
example: "abc123-def456"
fileId:
type: string
example: "f3e8d1a4"
expiresInMinutes:
type: integer
example: 15
requiredHeaders:
type: object
example: {"Content-Type": "image/png"}
POST /api/v1/logos/{uploadId}/confirm
parameters:
- name: uploadId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
required: [fileExtension]
properties:
fileExtension:
type: string
enum: [png, jpg, jpeg, svg]
example: "png"
checksum:
type: string
example: "sha256:abc123..."
responses:
'200':
description: Upload confirmed
content:
application/json:
schema:
type: object
properties:
uploadId:
type: string
status:
type: string
enum: [CONFIRMED]
POST /api/v1/companies (Updated)
requestBody:
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
displayName:
type: string
logoUploadId: # NEW FIELD
type: string
description: "Upload ID from /logos/presigned-url"
example: "abc123-def456"
# ... other fields
Testing Requirements
Unit Test Coverage: >90% for service classes
- GenericLogoServiceTest
- LogoCleanupServiceTest
Integration Test Coverage: >80% for controllers
- LogoControllerTest (Spring Boot Test)
- CompanyWithLogoIntegrationTest
E2E Tests:
- Create company with logo (single operation)
- Upload logo, wait, then create company
- Verify cleanup job removes orphaned uploads
Performance Tests:
- Presigned URL generation < 100ms
- S3 copy operation < 2s
- Cleanup job processes 1000+ logos in < 30s
Mock Configuration:
@MockBean
private S3Presigner s3Presigner;
@MockBean
private S3Client s3Client;
@TestConfiguration
static class TestConfig {
@Bean
public S3Presigner mockS3Presigner() {
S3Presigner presigner = mock(S3Presigner.class);
// Configure mock presigned URL generation
return presigner;
}
}
Schema Validation Test
Location: services/company-user-management-service/src/test/java/ch/batbern/companyuser/validation/LogoSchemaValidationTest.java
@SpringBootTest
@Testcontainers
class LogoSchemaValidationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
@Autowired
private EntityManager entityManager;
@Test
void shouldValidateLogoEntityMatchesMigration() {
// Verify all @Column annotations match V6 migration
// Verify indexes exist
// Verify constraints match
}
}
Definition of Done Checklist
Development Complete
Schema Validation (For JPA/Database stories)
Migration Alignment:
Entity Validation:
Documentation:
Infrastructure Complete
Frontend Complete
Review Ready
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2025-10-27 | 1.0 | Initial story creation | Winston (Architect Agent) |
Dev Agent Record
Agent Model Used
Claude Sonnet 4.5 (claude-sonnet-4-5-20250929)
Implementation Approach
TDD approach following Red-Green-Refactor cycle:
- Create migration first (V6)
- Write failing tests for each component
- Implement minimal code to pass tests
- Refactor while keeping tests green
Debug Log References
(To be filled during implementation)
Completion Notes
(To be filled during implementation)
Files Changed
Created:
- Backend: Logo.java, LogoStatus.java, LogoRepository.java, GenericLogoService.java, LogoCleanupService.java, LogoController.java, V6__Create_logos_table.sql
- Frontend: FileUpload.tsx, useFileUpload.ts, tests
Modified:
- CreateCompanyRequest.java, CompanyService.java, CompanyController.java, CompanyForm.tsx
Deleted:
- CompanyLogoService.java, LogoUpload.tsx, LogoUpload.test.tsx
Deployment Notes
Deployment Order:
- Deploy backend with new endpoints (API backward compatible during transition)
- Deploy frontend with new FileUpload component
- Monitor cleanup job execution
- Verify no orphaned uploads accumulate
Rollback Procedure:
- Revert backend deployment
- Database: logos table can remain (unused)
- S3: No cleanup needed (orphans will be removed by cleanup job)
QA Results
(To be filled by QA Agent after implementation)