"""Tests for DATEV export module: export creation, CSV format, date range validation.""" import csv import io import uuid from datetime import date from decimal import Decimal import pytest_asyncio from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.models.sale import Sale from app.models.vehicle import Vehicle from app.models.contact import Contact from app.utils.datev import generate_datev_csv, validate_datev_csv, DATEV_HEADERS @pytest_asyncio.fixture async def test_vehicle_for_datev(db_session: AsyncSession) -> Vehicle: """Create a test vehicle for DATEV tests.""" vehicle = Vehicle( make="MAN", model="TGX", fin="WMA12345678901234", year=2021, vehicle_type="lkw", price=Decimal("55000.00"), availability="available", condition="used", ) db_session.add(vehicle) await db_session.commit() await db_session.refresh(vehicle) return vehicle @pytest_asyncio.fixture async def test_buyer_for_datev(db_session: AsyncSession) -> Contact: """Create a test buyer contact for DATEV tests.""" contact = Contact( company_name="DATEV Test Buyer GmbH", role="kaeufer", address_country="DE", vat_id="DE111222333", address_street="Testweg 3", address_zip="67890", address_city="Hamburg", ) db_session.add(contact) await db_session.commit() await db_session.refresh(contact) return contact @pytest_asyncio.fixture async def test_completed_sales( db_session: AsyncSession, test_vehicle_for_datev, test_buyer_for_datev ) -> list[Sale]: """Create test completed sales within a date range.""" sales = [] for i, (day, price) in enumerate( [(5, Decimal("30000.00")), (10, Decimal("25000.00")), (15, Decimal("40000.00"))] ): sale = Sale( vehicle_id=test_vehicle_for_datev.id, buyer_contact_id=test_buyer_for_datev.id, sale_price=price, sale_date=date(2025, 1, day), status="completed", is_gwg=False, ) db_session.add(sale) sales.append(sale) await db_session.commit() for s in sales: await db_session.refresh(s) return sales class TestDATEVExportAPI: """Test DATEV export API endpoints.""" async def test_create_export(self, admin_client: AsyncClient, test_completed_sales): """POST /datev/export with valid date range returns 201.""" response = await admin_client.post( "/api/v1/datev/export", json={ "start_date": "2025-01-01", "end_date": "2025-01-31", }, ) assert response.status_code == 201 data = response.json() assert data["start_date"] == "2025-01-01" assert data["end_date"] == "2025-01-31" assert data["file_path"] is not None assert float(data["total_amount"]) == 95000.00 async def test_create_export_invalid_date_range(self, admin_client: AsyncClient): """POST /datev/export with start > end returns 422.""" response = await admin_client.post( "/api/v1/datev/export", json={ "start_date": "2025-12-31", "end_date": "2025-01-01", }, ) assert response.status_code == 422 async def test_list_exports(self, admin_client: AsyncClient, test_completed_sales): """GET /datev/exports returns list of exports.""" # First create an export await admin_client.post( "/api/v1/datev/export", json={ "start_date": "2025-01-01", "end_date": "2025-01-31", }, ) response = await admin_client.get("/api/v1/datev/exports") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 assert len(data["items"]) >= 1 assert "start_date" in data["items"][0] assert "end_date" in data["items"][0] async def test_download_export_csv( self, admin_client: AsyncClient, test_completed_sales ): """GET /datev/exports/:id/download returns CSV content.""" # Create export create_response = await admin_client.post( "/api/v1/datev/export", json={ "start_date": "2025-01-01", "end_date": "2025-01-31", }, ) assert create_response.status_code == 201 export_id = create_response.json()["id"] # Download response = await admin_client.get(f"/api/v1/datev/exports/{export_id}/download") assert response.status_code == 200 assert response.headers["content-type"] == "text/csv; charset=utf-8" # Parse CSV and verify headers csv_content = response.text reader = csv.reader(io.StringIO(csv_content), delimiter=";") header = next(reader) assert header == DATEV_HEADERS # Verify at least one data row rows = list(reader) assert len(rows) >= 1 for row in rows: assert len(row) == 6 async def test_download_export_not_found(self, admin_client: AsyncClient): """GET /datev/exports/:nonexistent/download returns 404.""" response = await admin_client.get( f"/api/v1/datev/exports/{uuid.uuid4()}/download" ) assert response.status_code == 404 class TestDATEVCSVFormat: """Test DATEV CSV format helper functions.""" def test_datev_csv_headers(self): """DATEV CSV has correct headers.""" assert DATEV_HEADERS == [ "Datum", "Konto", "Gegenkonto", "Betrag", "Belegfeld", "Buchungstext", ] def test_generate_datev_csv_empty(self): """Generate DATEV CSV with no sales returns only headers.""" csv_content = generate_datev_csv([]) assert validate_datev_csv(csv_content) reader = csv.reader(io.StringIO(csv_content), delimiter=";") header = next(reader) assert header == DATEV_HEADERS # No data rows rows = list(reader) assert len(rows) == 0 def test_generate_datev_csv_with_sales(self, test_completed_sales): """Generate DATEV CSV with sales produces correct rows.""" # test_completed_sales is a fixture but we need to call it differently for sync test # Instead, create mock objects class MockVehicle: make = "MAN" model = "TGX" fin = "WMA12345678901234" class MockSale: def __init__(self, sale_id, sale_date, sale_price): self.id = sale_id self.sale_date = sale_date self.sale_price = sale_price self.vehicle = MockVehicle() sales = [ MockSale(uuid.uuid4(), date(2025, 1, 5), Decimal("30000.00")), MockSale(uuid.uuid4(), date(2025, 1, 10), Decimal("25000.00")), ] csv_content = generate_datev_csv(sales) assert validate_datev_csv(csv_content) reader = csv.reader(io.StringIO(csv_content), delimiter=";") header = next(reader) assert header == DATEV_HEADERS rows = list(reader) assert len(rows) == 2 # Check first row format row1 = rows[0] assert row1[0] == "05.01.2025" # Datum in DD.MM.YYYY assert row1[1] == "1200" # Konto assert row1[2] == "8400" # Gegenkonto assert "," in row1[3] # Betrag with comma separator assert row1[4] # Belegfeld (short UUID) assert "MAN" in row1[5] # Buchungstext contains vehicle make def test_validate_datev_csv_invalid_headers(self): """Validate DATEV CSV with wrong headers returns False.""" csv_content = "Wrong;Headers;Here\nval1;val2;val3" assert not validate_datev_csv(csv_content) def test_validate_datev_csv_empty(self): """Validate empty CSV returns False.""" assert not validate_datev_csv("") assert not validate_datev_csv(" ") def test_validate_datev_csv_valid(self): """Validate correct DATEV CSV returns True.""" csv_content = "Datum;Konto;Gegenkonto;Betrag;Belegfeld;Buchungstext\n05.01.2025;1200;8400;30000,00;ABC12345;MAN TGX" assert validate_datev_csv(csv_content) def test_datev_csv_amount_format(self): """DATEV CSV amount uses comma as decimal separator.""" class MockVehicle: make = "VW" model = "Crafter" fin = "WVW12345678901234" class MockSale: def __init__(self): self.id = uuid.uuid4() self.sale_date = date(2025, 3, 15) self.sale_price = Decimal("12345.67") self.vehicle = MockVehicle() csv_content = generate_datev_csv([MockSale()]) reader = csv.reader(io.StringIO(csv_content), delimiter=";") next(reader) # skip header row = next(reader) assert row[3] == "12345,67" # Comma as decimal separator class TestDATEVExportService: """Test DATEV export service directly.""" async def test_create_export_no_sales_in_range(self, db_session: AsyncSession): """Creating export with no sales in range produces empty CSV with 0 total.""" from app.services import datev_service export = await datev_service.create_export( db_session, start_date=date(2025, 6, 1), end_date=date(2025, 6, 30), ) assert export.total_amount == Decimal("0") assert export.file_path is not None async def test_list_exports_pagination(self, db_session: AsyncSession): """List exports with pagination.""" from app.services import datev_service # Create a few exports for _ in range(3): await datev_service.create_export( db_session, start_date=date(2025, 1, 1), end_date=date(2025, 1, 31), ) exports, total = await datev_service.list_exports( db_session, page=1, page_size=2 ) assert total >= 3 assert len(exports) <= 2