# tests/integration/api/v1/vendor/test_invoices.py """Integration tests for vendor invoice management endpoints. Tests the /api/v1/vendor/invoices/* endpoints. All endpoints require vendor JWT authentication. """ from decimal import Decimal import pytest from models.database.customer import Customer from models.database.invoice import Invoice, InvoiceStatus, VendorInvoiceSettings from models.database.order import Order @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceSettingsAPI: """Test vendor invoice settings endpoints at /api/v1/vendor/invoices/settings.""" def test_get_settings_not_configured(self, client, vendor_user_headers): """Test getting settings when not configured returns null.""" response = client.get( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, ) assert response.status_code == 200 assert response.json() is None def test_create_settings_success(self, client, vendor_user_headers): """Test creating invoice settings successfully.""" settings_data = { "company_name": "Test Company S.A.", "company_address": "123 Test Street", "company_city": "Luxembourg", "company_postal_code": "L-1234", "company_country": "LU", "vat_number": "LU12345678", "invoice_prefix": "INV", "default_vat_rate": 17.0, "bank_name": "BCEE", "bank_iban": "LU123456789012345678", "bank_bic": "BCEELULL", "payment_terms": "Net 30 days", } response = client.post( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, json=settings_data, ) assert response.status_code == 201, f"Failed: {response.json()}" data = response.json() assert data["company_name"] == "Test Company S.A." assert data["company_country"] == "LU" assert data["invoice_prefix"] == "INV" def test_create_settings_minimal(self, client, vendor_user_headers): """Test creating settings with minimal required data.""" settings_data = { "company_name": "Minimal Company", } response = client.post( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, json=settings_data, ) assert response.status_code == 201 data = response.json() assert data["company_name"] == "Minimal Company" # Defaults should be applied assert data["invoice_prefix"] == "INV" def test_create_settings_duplicate_fails( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test creating duplicate settings fails.""" # Create settings directly in DB settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="Existing Company", company_country="LU", ) db.add(settings) db.commit() response = client.post( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, json={"company_name": "New Company"}, ) # ValidationException returns 422 assert response.status_code == 422 def test_get_settings_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test getting existing settings.""" # Create settings settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="Get Settings Company", company_country="LU", invoice_prefix="FAC", ) db.add(settings) db.commit() response = client.get( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["company_name"] == "Get Settings Company" assert data["invoice_prefix"] == "FAC" def test_update_settings_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test updating invoice settings.""" # Create settings settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="Original Company", company_country="LU", ) db.add(settings) db.commit() update_data = { "company_name": "Updated Company", "bank_iban": "LU999888777666555444", } response = client.put( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, json=update_data, ) assert response.status_code == 200 data = response.json() assert data["company_name"] == "Updated Company" assert data["bank_iban"] == "LU999888777666555444" def test_update_settings_not_found(self, client, vendor_user_headers): """Test updating non-existent settings returns error.""" response = client.put( "/api/v1/vendor/invoices/settings", headers=vendor_user_headers, json={"company_name": "Updated"}, ) assert response.status_code == 404 def test_settings_without_auth_returns_unauthorized(self, client): """Test accessing settings without auth returns 401.""" response = client.get("/api/v1/vendor/invoices/settings") assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceStatsAPI: """Test vendor invoice statistics endpoint at /api/v1/vendor/invoices/stats.""" def test_get_stats_empty(self, client, vendor_user_headers): """Test getting stats when no invoices exist.""" response = client.get( "/api/v1/vendor/invoices/stats", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["total_invoices"] == 0 assert data["total_revenue_cents"] == 0 assert data["draft_count"] == 0 assert data["paid_count"] == 0 def test_get_stats_with_invoices( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test getting stats with existing invoices.""" from datetime import datetime, UTC # Create invoices for i, status in enumerate([InvoiceStatus.DRAFT, InvoiceStatus.PAID]): invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number=f"INV0000{i+1}", invoice_date=datetime.now(UTC), status=status.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000 * (i + 1), vat_amount_cents=1700 * (i + 1), total_cents=11700 * (i + 1), ) db.add(invoice) db.commit() response = client.get( "/api/v1/vendor/invoices/stats", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["total_invoices"] == 2 assert data["draft_count"] == 1 assert data["paid_count"] == 1 def test_stats_without_auth_returns_unauthorized(self, client): """Test accessing stats without auth returns 401.""" response = client.get("/api/v1/vendor/invoices/stats") assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceListAPI: """Test vendor invoice list endpoint at /api/v1/vendor/invoices.""" def test_list_invoices_empty(self, client, vendor_user_headers): """Test listing invoices when none exist.""" response = client.get( "/api/v1/vendor/invoices", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["items"] == [] assert data["total"] == 0 assert data["page"] == 1 def test_list_invoices_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test listing invoices successfully.""" from datetime import datetime, UTC # Create invoices for i in range(3): invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number=f"INV0000{i+1}", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Test"}, buyer_details={"name": f"Buyer {i+1}"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() response = client.get( "/api/v1/vendor/invoices", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert len(data["items"]) == 3 assert data["total"] == 3 def test_list_invoices_with_status_filter( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test filtering invoices by status.""" from datetime import datetime, UTC # Create invoices with different statuses for status in [InvoiceStatus.DRAFT, InvoiceStatus.ISSUED, InvoiceStatus.PAID]: invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number=f"INV-{status.value.upper()}", invoice_date=datetime.now(UTC), status=status.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() # Filter by paid status response = client.get( "/api/v1/vendor/invoices?status=paid", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["items"][0]["status"] == "paid" def test_list_invoices_pagination( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test invoice list pagination.""" from datetime import datetime, UTC # Create 5 invoices for i in range(5): invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number=f"INV0000{i+1}", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() # Get first page with 2 items response = client.get( "/api/v1/vendor/invoices?page=1&per_page=2", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert len(data["items"]) == 2 assert data["total"] == 5 assert data["page"] == 1 assert data["pages"] == 3 def test_list_invoices_without_auth_returns_unauthorized(self, client): """Test listing invoices without auth returns 401.""" response = client.get("/api/v1/vendor/invoices") assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceDetailAPI: """Test vendor invoice detail endpoint at /api/v1/vendor/invoices/{id}.""" def test_get_invoice_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test getting invoice by ID.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Seller Co"}, buyer_details={"name": "John Doe"}, line_items=[ { "description": "Product A", "quantity": 2, "unit_price_cents": 5000, "total_cents": 10000, } ], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.get( f"/api/v1/vendor/invoices/{invoice.id}", headers=vendor_user_headers, ) assert response.status_code == 200 data = response.json() assert data["invoice_number"] == "INV00001" assert data["total_cents"] == 11700 assert len(data["line_items"]) == 1 def test_get_invoice_not_found(self, client, vendor_user_headers): """Test getting non-existent invoice returns 404.""" response = client.get( "/api/v1/vendor/invoices/99999", headers=vendor_user_headers, ) assert response.status_code == 404 def test_get_invoice_without_auth_returns_unauthorized(self, client): """Test getting invoice without auth returns 401.""" response = client.get("/api/v1/vendor/invoices/1") assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceCreateAPI: """Test vendor invoice creation endpoint at /api/v1/vendor/invoices.""" def test_create_invoice_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user, ): """Test creating invoice from order.""" from datetime import datetime, UTC now = datetime.now(UTC) # Create invoice settings first settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="Test Company", company_country="LU", invoice_prefix="INV", invoice_next_number=1, ) db.add(settings) db.commit() # Create a customer first customer = Customer( vendor_id=test_vendor_with_vendor_user.id, email="test@example.com", hashed_password="$2b$12$test_hashed_password", first_name="John", last_name="Doe", customer_number="CUST-001", ) db.add(customer) db.commit() db.refresh(customer) # Create an order order = Order( vendor_id=test_vendor_with_vendor_user.id, customer_id=customer.id, order_number="ORD-001", channel="direct", order_date=now, customer_first_name="John", customer_last_name="Doe", customer_email="test@example.com", ship_first_name="John", ship_last_name="Doe", ship_address_line_1="123 Test St", ship_city="Luxembourg", ship_postal_code="L-1234", ship_country_iso="LU", bill_first_name="John", bill_last_name="Doe", bill_address_line_1="123 Test St", bill_city="Luxembourg", bill_postal_code="L-1234", bill_country_iso="LU", currency="EUR", status="completed", subtotal_cents=10000, total_amount_cents=11700, ) db.add(order) db.commit() db.refresh(order) # Create invoice (without order items - service handles empty items) response = client.post( "/api/v1/vendor/invoices", headers=vendor_user_headers, json={"order_id": order.id, "notes": "Test invoice"}, ) assert response.status_code == 201, f"Failed: {response.json()}" data = response.json() assert data["order_id"] == order.id assert data["invoice_number"] == "INV00001" assert data["status"] == "draft" assert data["notes"] == "Test invoice" def test_create_invoice_without_settings_fails( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test creating invoice without settings configured fails.""" from datetime import datetime, UTC now = datetime.now(UTC) # Create a customer first customer = Customer( vendor_id=test_vendor_with_vendor_user.id, email="jane@example.com", hashed_password="$2b$12$test_hashed_password", first_name="Jane", last_name="Doe", customer_number="CUST-002", ) db.add(customer) db.commit() db.refresh(customer) # Create an order without settings order = Order( vendor_id=test_vendor_with_vendor_user.id, customer_id=customer.id, order_number="ORD-002", channel="direct", order_date=now, customer_first_name="Jane", customer_last_name="Doe", customer_email="jane@example.com", ship_first_name="Jane", ship_last_name="Doe", ship_address_line_1="456 Test Ave", ship_city="Luxembourg", ship_postal_code="L-5678", ship_country_iso="LU", bill_first_name="Jane", bill_last_name="Doe", bill_address_line_1="456 Test Ave", bill_city="Luxembourg", bill_postal_code="L-5678", bill_country_iso="LU", currency="EUR", status="completed", subtotal_cents=10000, total_amount_cents=11700, ) db.add(order) db.commit() db.refresh(order) response = client.post( "/api/v1/vendor/invoices", headers=vendor_user_headers, json={"order_id": order.id}, ) assert response.status_code == 404 # Settings not found def test_create_invoice_order_not_found( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test creating invoice for non-existent order fails.""" # Create settings settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="Test Company", company_country="LU", ) db.add(settings) db.commit() response = client.post( "/api/v1/vendor/invoices", headers=vendor_user_headers, json={"order_id": 99999}, ) assert response.status_code == 404 def test_create_invoice_without_auth_returns_unauthorized(self, client): """Test creating invoice without auth returns 401.""" response = client.post( "/api/v1/vendor/invoices", json={"order_id": 1}, ) assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoiceStatusAPI: """Test vendor invoice status update endpoint.""" def test_update_status_to_issued( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test updating invoice status to issued.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.put( f"/api/v1/vendor/invoices/{invoice.id}/status", headers=vendor_user_headers, json={"status": "issued"}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "issued" def test_update_status_to_paid( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test updating invoice status to paid.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.ISSUED.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.put( f"/api/v1/vendor/invoices/{invoice.id}/status", headers=vendor_user_headers, json={"status": "paid"}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "paid" def test_update_status_to_cancelled( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test cancelling an invoice.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.put( f"/api/v1/vendor/invoices/{invoice.id}/status", headers=vendor_user_headers, json={"status": "cancelled"}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "cancelled" def test_update_cancelled_invoice_fails( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test updating cancelled invoice fails.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.CANCELLED.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.put( f"/api/v1/vendor/invoices/{invoice.id}/status", headers=vendor_user_headers, json={"status": "issued"}, ) # ValidationException returns 422 assert response.status_code == 422 def test_update_status_invalid_status( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test updating with invalid status fails.""" from datetime import datetime, UTC invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "Test"}, buyer_details={"name": "Buyer"}, line_items=[], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.put( f"/api/v1/vendor/invoices/{invoice.id}/status", headers=vendor_user_headers, json={"status": "invalid_status"}, ) # ValidationException returns 422 assert response.status_code == 422 def test_update_status_not_found(self, client, vendor_user_headers): """Test updating non-existent invoice fails.""" response = client.put( "/api/v1/vendor/invoices/99999/status", headers=vendor_user_headers, json={"status": "issued"}, ) assert response.status_code == 404 def test_update_status_without_auth_returns_unauthorized(self, client): """Test updating status without auth returns 401.""" response = client.put( "/api/v1/vendor/invoices/1/status", json={"status": "issued"}, ) assert response.status_code == 401 @pytest.mark.integration @pytest.mark.api @pytest.mark.vendor class TestVendorInvoicePDFAPI: """Test vendor invoice PDF endpoints.""" @pytest.mark.skip(reason="WeasyPrint not installed in test environment") def test_generate_pdf_success( self, client, vendor_user_headers, db, test_vendor_with_vendor_user ): """Test generating PDF for an invoice.""" from datetime import datetime, UTC # Create settings settings = VendorInvoiceSettings( vendor_id=test_vendor_with_vendor_user.id, company_name="PDF Test Company", company_country="LU", ) db.add(settings) db.commit() invoice = Invoice( vendor_id=test_vendor_with_vendor_user.id, invoice_number="INV00001", invoice_date=datetime.now(UTC), status=InvoiceStatus.DRAFT.value, seller_details={"company_name": "PDF Test Company"}, buyer_details={"name": "Buyer"}, line_items=[ { "description": "Test Item", "quantity": 1, "unit_price_cents": 10000, "total_cents": 10000, } ], vat_rate=Decimal("17.00"), subtotal_cents=10000, vat_amount_cents=1700, total_cents=11700, ) db.add(invoice) db.commit() db.refresh(invoice) response = client.post( f"/api/v1/vendor/invoices/{invoice.id}/pdf", headers=vendor_user_headers, ) assert response.status_code == 200 def test_generate_pdf_not_found(self, client, vendor_user_headers): """Test generating PDF for non-existent invoice fails.""" response = client.post( "/api/v1/vendor/invoices/99999/pdf", headers=vendor_user_headers, ) assert response.status_code == 404 def test_download_pdf_not_found(self, client, vendor_user_headers): """Test downloading PDF for non-existent invoice fails.""" response = client.get( "/api/v1/vendor/invoices/99999/pdf", headers=vendor_user_headers, ) assert response.status_code == 404 def test_pdf_endpoints_without_auth_returns_unauthorized(self, client): """Test PDF endpoints without auth return 401.""" response = client.post("/api/v1/vendor/invoices/1/pdf") assert response.status_code == 401 response = client.get("/api/v1/vendor/invoices/1/pdf") assert response.status_code == 401