feat(data-entry): Bulk Receipt Upload cu Mobile UX Android Nativ
## 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>
This commit is contained in:
642
tests/backend/test_receipt_auto_create.py
Normal file
642
tests/backend/test_receipt_auto_create.py
Normal file
@@ -0,0 +1,642 @@
|
||||
"""
|
||||
Tests for ReceiptAutoCreateService (US-009).
|
||||
|
||||
US-009 Acceptance Criteria:
|
||||
- Clasa ReceiptAutoCreateService cu metoda create_from_ocr_result(job_id, ocr_result, user, batch_id)
|
||||
- Mapare completă OCR fields → Receipt fields (vezi ExtractionData schema)
|
||||
- Creare attachment cu fișierul original din job.file_path
|
||||
- Generare accounting entries via AccountingService.generate_entries() existent
|
||||
- Validare minimă: suma > 0, dată validă (nu în viitor)
|
||||
- Return ReceiptCreateResult cu receipt_id sau error_message
|
||||
- Actualizează batch_jobs cu receipt_id după creare
|
||||
- pytest tests pass
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from backend.modules.data_entry.services.receipt_auto_create import (
|
||||
ReceiptAutoCreateService,
|
||||
ReceiptCreateResult,
|
||||
)
|
||||
from backend.modules.data_entry.schemas.ocr import ExtractionData, TvaEntry, PaymentMethod
|
||||
from backend.modules.data_entry.db.models.receipt import ReceiptType, ReceiptDirection
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def valid_ocr_result():
|
||||
"""Create a valid OCR extraction result."""
|
||||
return ExtractionData(
|
||||
receipt_type="bon_fiscal",
|
||||
receipt_number="123456",
|
||||
receipt_series="ABC",
|
||||
receipt_date=date.today() - timedelta(days=1), # Yesterday
|
||||
amount=Decimal("125.50"),
|
||||
partner_name="MEGA IMAGE SRL",
|
||||
cui="12345678",
|
||||
description="Produse alimentare",
|
||||
tva_entries=[
|
||||
TvaEntry(code="A", percent=19, amount=Decimal("20.00")),
|
||||
TvaEntry(code="B", percent=9, amount=Decimal("5.50")),
|
||||
],
|
||||
tva_total=Decimal("25.50"),
|
||||
address="Str. Test Nr. 1, Bucuresti",
|
||||
items_count=5,
|
||||
payment_methods=[
|
||||
PaymentMethod(method="CARD", amount=Decimal("125.50")),
|
||||
],
|
||||
suggested_payment_mode="banca",
|
||||
confidence_amount=0.95,
|
||||
confidence_date=0.90,
|
||||
confidence_vendor=0.85,
|
||||
overall_confidence=0.90,
|
||||
raw_text="BON FISCAL\nMEGA IMAGE SRL\nTotal: 125.50 LEI",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_ocr_result():
|
||||
"""Create minimal valid OCR result with only required fields."""
|
||||
return ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("50.00"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Create a mock async session."""
|
||||
session = AsyncMock()
|
||||
session.add = MagicMock()
|
||||
session.flush = AsyncMock()
|
||||
session.commit = AsyncMock()
|
||||
session.rollback = AsyncMock()
|
||||
session.execute = AsyncMock()
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
"""Create a temporary file for attachment testing."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
|
||||
f.write(b"fake pdf content for testing")
|
||||
temp_path = f.name
|
||||
yield temp_path
|
||||
# Cleanup
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Unit Tests - Validation
|
||||
# ============================================================================
|
||||
|
||||
class TestValidation:
|
||||
"""Test OCR result validation."""
|
||||
|
||||
def test_validate_valid_result(self, valid_ocr_result):
|
||||
"""Test validation passes for valid OCR result."""
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(valid_ocr_result)
|
||||
assert is_valid is True
|
||||
assert error == ""
|
||||
|
||||
def test_validate_missing_amount(self):
|
||||
"""Test validation fails when amount is missing."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=None,
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is False
|
||||
assert "amount" in error.lower()
|
||||
|
||||
def test_validate_zero_amount(self):
|
||||
"""Test validation fails when amount is zero."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("0"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is False
|
||||
assert "amount" in error.lower()
|
||||
|
||||
def test_validate_negative_amount(self):
|
||||
"""Test validation fails when amount is negative."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("-10.00"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is False
|
||||
assert "amount" in error.lower()
|
||||
|
||||
def test_validate_missing_date(self):
|
||||
"""Test validation fails when date is missing."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=None,
|
||||
amount=Decimal("100.00"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is False
|
||||
assert "date" in error.lower()
|
||||
|
||||
def test_validate_future_date(self):
|
||||
"""Test validation fails when date is in the future."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today() + timedelta(days=1), # Tomorrow
|
||||
amount=Decimal("100.00"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is False
|
||||
assert "future" in error.lower()
|
||||
|
||||
def test_validate_today_date_passes(self):
|
||||
"""Test validation passes when date is today."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("100.00"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is True
|
||||
assert error == ""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Unit Tests - Field Mapping
|
||||
# ============================================================================
|
||||
|
||||
class TestFieldMapping:
|
||||
"""Test OCR to Receipt field mapping."""
|
||||
|
||||
def test_map_basic_fields(self, valid_ocr_result):
|
||||
"""Test basic field mapping from OCR to Receipt."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert receipt_data.receipt_number == "123456"
|
||||
assert receipt_data.receipt_series == "ABC"
|
||||
assert receipt_data.receipt_date == valid_ocr_result.receipt_date
|
||||
assert receipt_data.amount == Decimal("125.50")
|
||||
assert receipt_data.partner_name == "MEGA IMAGE SRL"
|
||||
assert receipt_data.cui == "12345678"
|
||||
assert receipt_data.description == "Produse alimentare"
|
||||
assert receipt_data.company_id == 1
|
||||
|
||||
def test_map_receipt_type_bon_fiscal(self, valid_ocr_result):
|
||||
"""Test mapping bon_fiscal type correctly."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.receipt_type == ReceiptType.BON_FISCAL
|
||||
|
||||
def test_map_receipt_type_chitanta(self, valid_ocr_result):
|
||||
"""Test mapping chitanta type correctly."""
|
||||
valid_ocr_result.receipt_type = "chitanta"
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.receipt_type == ReceiptType.CHITANTA
|
||||
|
||||
def test_map_default_direction_is_expense(self, valid_ocr_result):
|
||||
"""Test default direction is expense (cheltuiala)."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.direction == ReceiptDirection.CHELTUIALA
|
||||
|
||||
def test_map_tva_breakdown(self, valid_ocr_result):
|
||||
"""Test TVA breakdown mapping."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert receipt_data.tva_breakdown is not None
|
||||
assert len(receipt_data.tva_breakdown) == 2
|
||||
assert receipt_data.tva_breakdown[0].code == "A"
|
||||
assert receipt_data.tva_breakdown[0].percent == 19
|
||||
assert receipt_data.tva_breakdown[0].amount == Decimal("20.00")
|
||||
assert receipt_data.tva_total == Decimal("25.50")
|
||||
|
||||
def test_map_payment_methods(self, valid_ocr_result):
|
||||
"""Test payment methods mapping."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert receipt_data.payment_methods is not None
|
||||
assert len(receipt_data.payment_methods) == 1
|
||||
assert receipt_data.payment_methods[0].method == "CARD"
|
||||
assert receipt_data.payment_methods[0].amount == Decimal("125.50")
|
||||
assert receipt_data.payment_mode == "banca"
|
||||
|
||||
def test_map_vendor_address(self, valid_ocr_result):
|
||||
"""Test vendor address mapping."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.vendor_address == "Str. Test Nr. 1, Bucuresti"
|
||||
|
||||
def test_map_items_count(self, valid_ocr_result):
|
||||
"""Test items count mapping."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.items_count == 5
|
||||
|
||||
def test_map_raw_text_truncated(self, valid_ocr_result):
|
||||
"""Test raw text is truncated to 5000 chars."""
|
||||
valid_ocr_result.raw_text = "X" * 10000
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=valid_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert len(receipt_data.ocr_raw_text) == 5000
|
||||
|
||||
def test_map_minimal_result(self, minimal_ocr_result):
|
||||
"""Test mapping works with minimal required fields."""
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=minimal_ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert receipt_data.amount == Decimal("50.00")
|
||||
assert receipt_data.receipt_date == date.today()
|
||||
assert receipt_data.company_id == 1
|
||||
assert receipt_data.receipt_number is None
|
||||
assert receipt_data.partner_name is None
|
||||
assert receipt_data.tva_breakdown is None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Unit Tests - ReceiptCreateResult
|
||||
# ============================================================================
|
||||
|
||||
class TestReceiptCreateResult:
|
||||
"""Test ReceiptCreateResult dataclass."""
|
||||
|
||||
def test_success_result(self):
|
||||
"""Test successful result has receipt_id."""
|
||||
result = ReceiptCreateResult(success=True, receipt_id=42)
|
||||
assert result.success is True
|
||||
assert result.receipt_id == 42
|
||||
assert result.error_message is None
|
||||
|
||||
def test_failure_result(self):
|
||||
"""Test failure result has error_message."""
|
||||
result = ReceiptCreateResult(success=False, error_message="Validation failed")
|
||||
assert result.success is False
|
||||
assert result.receipt_id is None
|
||||
assert result.error_message == "Validation failed"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Integration Tests with Mocked Dependencies
|
||||
# ============================================================================
|
||||
|
||||
class TestCreateFromOcrResult:
|
||||
"""Test create_from_ocr_result method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_creation(self, mock_session, valid_ocr_result, temp_file):
|
||||
"""Test successful receipt creation from OCR result."""
|
||||
# Mock ReceiptCRUD.create to return a mock receipt
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 1
|
||||
mock_receipt.amount = valid_ocr_result.amount
|
||||
mock_receipt.partner_name = valid_ocr_result.partner_name
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.AccountingEntryCRUD"
|
||||
) as mock_entry_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.settings"
|
||||
) as mock_settings:
|
||||
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
mock_entry_crud.create_bulk = AsyncMock()
|
||||
|
||||
# Mock settings for attachment path
|
||||
mock_settings.data_entry_upload_path_resolved = Path(tempfile.gettempdir())
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-123",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
file_path=temp_file,
|
||||
original_filename="bon.pdf",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.receipt_id == 1
|
||||
assert result.error_message is None
|
||||
mock_crud.create.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_failure_returns_error(self, mock_session):
|
||||
"""Test that validation failure returns error result."""
|
||||
invalid_ocr = ExtractionData(
|
||||
receipt_date=date.today() + timedelta(days=5), # Future date
|
||||
amount=Decimal("100.00"),
|
||||
)
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-123",
|
||||
ocr_result=invalid_ocr,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.error_message is not None
|
||||
assert "future" in result.error_message.lower()
|
||||
mock_session.commit.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zero_amount_returns_error(self, mock_session):
|
||||
"""Test that zero amount returns error result."""
|
||||
zero_amount_ocr = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("0"),
|
||||
)
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-123",
|
||||
ocr_result=zero_amount_ocr,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert "amount" in result.error_message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_job_updated_with_receipt_id(self, mock_session, valid_ocr_result):
|
||||
"""Test that batch_jobs table is updated with receipt_id."""
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 42
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.AccountingEntryCRUD"
|
||||
) as mock_entry_crud:
|
||||
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
mock_entry_crud.create_bulk = AsyncMock()
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-456",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
# Verify execute was called (for update batch_jobs)
|
||||
mock_session.execute.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accounting_entries_generated(self, mock_session, valid_ocr_result):
|
||||
"""Test that accounting entries are generated for the receipt."""
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 1
|
||||
mock_receipt.amount = valid_ocr_result.amount
|
||||
mock_receipt.direction = ReceiptDirection.CHELTUIALA
|
||||
mock_receipt.expense_type_code = None
|
||||
mock_receipt.payment_mode = "banca"
|
||||
mock_receipt.cash_register_account = None
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.AccountingEntryCRUD"
|
||||
) as mock_entry_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptService"
|
||||
) as mock_service:
|
||||
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
mock_entry_crud.create_bulk = AsyncMock()
|
||||
mock_service.generate_accounting_entries = MagicMock(return_value=[
|
||||
MagicMock(), # Mock entries
|
||||
])
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-789",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
mock_service.generate_accounting_entries.assert_called_once_with(mock_receipt)
|
||||
mock_entry_crud.create_bulk.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception_triggers_rollback(self, mock_session, valid_ocr_result):
|
||||
"""Test that exception during creation triggers rollback."""
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud:
|
||||
|
||||
mock_crud.create = AsyncMock(side_effect=Exception("Database error"))
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-err",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert "Database error" in result.error_message
|
||||
mock_session.rollback.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attachment_creation_failure_doesnt_fail_receipt(
|
||||
self, mock_session, valid_ocr_result
|
||||
):
|
||||
"""Test that attachment creation failure doesn't fail receipt creation."""
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 1
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.AccountingEntryCRUD"
|
||||
) as mock_entry_crud:
|
||||
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
mock_entry_crud.create_bulk = AsyncMock()
|
||||
|
||||
# Pass a non-existent file path
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-attach",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
file_path="/nonexistent/path/file.pdf",
|
||||
)
|
||||
|
||||
# Receipt should still be created successfully
|
||||
assert result.success is True
|
||||
assert result.receipt_id == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entry_generation_failure_doesnt_fail_receipt(
|
||||
self, mock_session, valid_ocr_result
|
||||
):
|
||||
"""Test that entry generation failure doesn't fail receipt creation."""
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 1
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptService"
|
||||
) as mock_service:
|
||||
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
mock_service.generate_accounting_entries = MagicMock(
|
||||
side_effect=Exception("Entry generation failed")
|
||||
)
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="test-job-entry-err",
|
||||
ocr_result=valid_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
# Receipt should still be created successfully
|
||||
assert result.success is True
|
||||
assert result.receipt_id == 1
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Edge Cases
|
||||
# ============================================================================
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and boundary conditions."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minimal_valid_ocr_creates_receipt(self, mock_session, minimal_ocr_result):
|
||||
"""Test that minimal valid OCR result creates receipt."""
|
||||
mock_receipt = MagicMock()
|
||||
mock_receipt.id = 1
|
||||
|
||||
with patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.ReceiptCRUD"
|
||||
) as mock_crud, patch(
|
||||
"backend.modules.data_entry.services.receipt_auto_create.AccountingEntryCRUD"
|
||||
):
|
||||
mock_crud.create = AsyncMock(return_value=mock_receipt)
|
||||
|
||||
result = await ReceiptAutoCreateService.create_from_ocr_result(
|
||||
session=mock_session,
|
||||
job_id="minimal-job",
|
||||
ocr_result=minimal_ocr_result,
|
||||
username="test_user",
|
||||
batch_id=1,
|
||||
company_id=1,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
|
||||
def test_empty_tva_entries_maps_to_none(self):
|
||||
"""Test that empty TVA entries list maps to None."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("100.00"),
|
||||
tva_entries=[],
|
||||
)
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.tva_breakdown is None
|
||||
|
||||
def test_empty_payment_methods_maps_to_none(self):
|
||||
"""Test that empty payment methods list maps to None."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("100.00"),
|
||||
payment_methods=[],
|
||||
)
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.payment_methods is None
|
||||
|
||||
def test_none_raw_text_handled(self):
|
||||
"""Test that None raw text is handled correctly."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("100.00"),
|
||||
raw_text="",
|
||||
)
|
||||
receipt_data = ReceiptAutoCreateService._map_ocr_to_receipt(
|
||||
ocr_result=ocr_result,
|
||||
company_id=1,
|
||||
)
|
||||
assert receipt_data.ocr_raw_text is None
|
||||
|
||||
def test_very_small_amount_valid(self):
|
||||
"""Test that very small positive amount is valid."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("0.01"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is True
|
||||
|
||||
def test_very_large_amount_valid(self):
|
||||
"""Test that very large amount is valid."""
|
||||
ocr_result = ExtractionData(
|
||||
receipt_date=date.today(),
|
||||
amount=Decimal("999999999.99"),
|
||||
)
|
||||
is_valid, error = ReceiptAutoCreateService._validate_ocr_result(ocr_result)
|
||||
assert is_valid is True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user