## 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>
643 lines
24 KiB
Python
643 lines
24 KiB
Python
"""
|
|
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"])
|