## Funcționalități Principale ### Bulk Upload & Processing - Drag & drop pentru upload bonuri multiple oriunde pe pagină - Batch processing cu job queue și worker pool - Real-time updates via SSE (Server-Sent Events) cu fallback polling - Duplicate detection via SHA-256 file hash - Auto-retry pentru job-uri failed - Cancel individual jobs sau batch complet ### Mobile UX - Android Native Style - Top bar fixă cu hamburger, titlu centrat, acțiuni (search/filter) - Bottom navigation cu 4 tab-uri (Bonuri, Upload, Rapoarte, Setări) - FAB (Floating Action Button) cu hide/show on scroll - Filter chips orizontal scrollabile - Selecție multiplă prin long-press (500ms) - Select All + Bulk Delete cu confirmare - Layout Android pentru Create/Edit/View bon (Gmail compose style) ### Bug Fixes - Refresh individual via SSE în loc de refresh total pagină - Bonurile cu eroare OCR rămân vizibile pentru editare manuală - Afișare nume fișier original pentru toate bonurile - Upload stabil pe mobil (fix race condition File API) - Păstrare ordine bonuri la refresh (nu se reordonează) ### Backend - SSE endpoint pentru status updates real-time - Bulk delete endpoint cu partial success - Auto-cleanup bonuri failed după 7 zile - Batch model cu tracking complet ### Testing - E2E tests cu Playwright - Unit tests pentru bulk upload, auto-create, cleanup ## Commits Squashed: 43 user stories (US-001 → US-043) ## Branch: ralph/bulk-receipt-upload ## Timp dezvoltare: ~3 zile (Ralph autonomous) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
213 lines
8.4 KiB
Python
213 lines
8.4 KiB
Python
"""Pydantic schemas for bulk upload endpoints."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class BulkUploadResponse(BaseModel):
|
|
"""Response schema for bulk upload endpoint."""
|
|
|
|
batch_id: int = Field(..., description="Unique batch identifier for tracking")
|
|
job_ids: List[str] = Field(..., description="List of OCR job UUIDs created")
|
|
total_files: int = Field(..., description="Number of files in the batch")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"batch_id": 1,
|
|
"job_ids": [
|
|
"550e8400-e29b-41d4-a716-446655440001",
|
|
"550e8400-e29b-41d4-a716-446655440002",
|
|
],
|
|
"total_files": 2,
|
|
"message": "2 files queued for processing"
|
|
}
|
|
}
|
|
|
|
|
|
class BatchJobInfo(BaseModel):
|
|
"""Information about a single job in a batch."""
|
|
|
|
job_id: str = Field(..., description="OCR job UUID")
|
|
filename: str = Field(..., description="Original filename")
|
|
status: str = Field(..., description="Job status: pending, processing, completed, failed")
|
|
receipt_id: Optional[int] = Field(None, description="Created receipt ID (if completed)")
|
|
error_message: Optional[str] = Field(None, description="Error message (if failed)")
|
|
|
|
|
|
class BatchStatusResponse(BaseModel):
|
|
"""Response schema for batch status endpoint."""
|
|
|
|
batch_id: int = Field(..., description="Batch identifier")
|
|
status: str = Field(..., description="Overall batch status")
|
|
total_files: int = Field(..., description="Total number of files in batch")
|
|
pending_count: int = Field(..., description="Number of pending jobs")
|
|
processing_count: int = Field(..., description="Number of processing jobs")
|
|
completed_count: int = Field(..., description="Number of completed jobs")
|
|
failed_count: int = Field(..., description="Number of failed jobs")
|
|
jobs: List[BatchJobInfo] = Field(..., description="List of jobs with their status")
|
|
total_amount: Optional[float] = Field(None, description="Sum of all receipt amounts")
|
|
created_at: datetime = Field(..., description="Batch creation timestamp")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"batch_id": 1,
|
|
"status": "processing",
|
|
"total_files": 5,
|
|
"pending_count": 2,
|
|
"processing_count": 1,
|
|
"completed_count": 2,
|
|
"failed_count": 0,
|
|
"jobs": [
|
|
{"job_id": "abc-123", "filename": "bon1.pdf", "status": "completed", "receipt_id": 15},
|
|
{"job_id": "def-456", "filename": "bon2.jpg", "status": "processing", "receipt_id": None},
|
|
],
|
|
"total_amount": 150.50,
|
|
"created_at": "2025-01-09T10:30:00"
|
|
}
|
|
}
|
|
|
|
|
|
class DuplicateFileInfo(BaseModel):
|
|
"""Information about a duplicate file detected during upload."""
|
|
|
|
filename: str = Field(..., description="Name of the duplicate file")
|
|
error: str = Field(default="duplicate", description="Error type (always 'duplicate')")
|
|
existing_receipt_id: int = Field(..., description="ID of the existing receipt with same file hash")
|
|
message: str = Field(..., description="Human-readable error message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"filename": "bon_lidl.pdf",
|
|
"error": "duplicate",
|
|
"existing_receipt_id": 123,
|
|
"message": "Fișier duplicat - există deja ca bon #123"
|
|
}
|
|
}
|
|
|
|
|
|
class BulkUploadResponseWithDuplicates(BaseModel):
|
|
"""Response schema for bulk upload with partial success (some duplicates)."""
|
|
|
|
batch_id: Optional[int] = Field(None, description="Batch ID (None if all files were duplicates)")
|
|
job_ids: List[str] = Field(default_factory=list, description="List of OCR job UUIDs created")
|
|
total_files: int = Field(..., description="Total number of files submitted")
|
|
processed_files: int = Field(..., description="Number of files successfully queued")
|
|
duplicate_files: int = Field(..., description="Number of duplicate files rejected")
|
|
duplicates: List[DuplicateFileInfo] = Field(default_factory=list, description="List of duplicate file details")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"batch_id": 1,
|
|
"job_ids": ["550e8400-e29b-41d4-a716-446655440001"],
|
|
"total_files": 3,
|
|
"processed_files": 1,
|
|
"duplicate_files": 2,
|
|
"duplicates": [
|
|
{
|
|
"filename": "bon_lidl.pdf",
|
|
"error": "duplicate",
|
|
"existing_receipt_id": 123,
|
|
"message": "Fișier duplicat - există deja ca bon #123"
|
|
}
|
|
],
|
|
"message": "1 fișier în procesare, 2 duplicate ignorate"
|
|
}
|
|
}
|
|
|
|
|
|
class BulkUploadError(BaseModel):
|
|
"""Error response for bulk upload validation failures."""
|
|
|
|
detail: str = Field(..., description="Error message")
|
|
invalid_files: Optional[List[str]] = Field(None, description="List of invalid filenames")
|
|
|
|
|
|
class RetryResponse(BaseModel):
|
|
"""Response schema for retry endpoints."""
|
|
|
|
success: bool = Field(..., description="Whether the retry was successful")
|
|
receipt_id: int = Field(..., description="Receipt ID that was retried")
|
|
job_id: Optional[str] = Field(None, description="New OCR job ID created")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"success": True,
|
|
"receipt_id": 123,
|
|
"job_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"message": "Bon reîncarcat în procesare"
|
|
}
|
|
}
|
|
|
|
|
|
class BatchRetryResponse(BaseModel):
|
|
"""Response schema for batch retry endpoint."""
|
|
|
|
success: bool = Field(..., description="Whether any retries were successful")
|
|
batch_id: str = Field(..., description="Batch ID that was retried")
|
|
retried_count: int = Field(..., description="Number of receipts successfully retried")
|
|
failed_count: int = Field(..., description="Number of receipts that couldn't be retried")
|
|
errors: List[str] = Field(default_factory=list, description="List of error messages")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"success": True,
|
|
"batch_id": "abc-123",
|
|
"retried_count": 3,
|
|
"failed_count": 0,
|
|
"errors": [],
|
|
"message": "3 bonuri reîncarcate în procesare"
|
|
}
|
|
}
|
|
|
|
|
|
class CancelJobResponse(BaseModel):
|
|
"""Response schema for cancel job endpoint."""
|
|
|
|
success: bool = Field(..., description="Whether the cancellation was successful")
|
|
job_id: str = Field(..., description="Job ID that was cancelled")
|
|
cancelled_at: datetime = Field(..., description="Timestamp when the job was cancelled")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"success": True,
|
|
"job_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"cancelled_at": "2025-01-11T15:30:00",
|
|
"message": "Job anulat cu succes"
|
|
}
|
|
}
|
|
|
|
|
|
class CancelBatchResponse(BaseModel):
|
|
"""Response schema for cancel batch endpoint."""
|
|
|
|
success: bool = Field(..., description="Whether any jobs were cancelled")
|
|
batch_id: int = Field(..., description="Batch ID that was cancelled")
|
|
cancelled_count: int = Field(..., description="Number of jobs successfully cancelled")
|
|
skipped_count: int = Field(..., description="Number of jobs skipped (completed/failed)")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"success": True,
|
|
"batch_id": 1,
|
|
"cancelled_count": 3,
|
|
"skipped_count": 2,
|
|
"message": "3 job-uri anulate, 2 ignorate (deja procesate)"
|
|
}
|
|
}
|