import asyncio import json import sys from datetime import date from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) def test_simply_import_service_persists_multi_line_orders_without_overwrite(monkeypatch): from app.modules.invoice_error_finder.services.simply_import_service import SimplyImportService captured = [] def fake_execute_query(query, params=None): if "INSERT INTO invoice_error_finder_simply_sales_orders" in query: captured.append(params) return [] monkeypatch.setattr( "app.modules.invoice_error_finder.services.simply_import_service.execute_query", fake_execute_query, ) service = SimplyImportService() service._persist_order( 7, { "id": "SO-14", "salesorder_no": "SO-14", "account_id": "A-1", "accountname": "Testkunde", "sostatus": "Approved", "LineItems": [ {"productnumber": "P1", "productname": "Linje 1", "quantity": 1, "listprice": 10, "netprice": 10}, {"productnumber": "P2", "productname": "Linje 2", "quantity": 2, "listprice": 20, "netprice": 40}, ], }, ) assert len(captured) == 2 assert captured[0][1] != captured[1][1] assert captured[0][1].startswith("SO-14:") assert captured[1][1].startswith("SO-14:") def test_economic_import_service_persists_valid_json(monkeypatch): from app.modules.invoice_error_finder.services.economic_import_service import EconomicImportService captured = {} def fake_execute_query_single(query, params=None): if "INSERT INTO invoice_error_finder_economic_invoices" in query: captured["invoice_raw"] = params[-1] return {"id": 11} return {"id": 11} monkeypatch.setattr( "app.modules.invoice_error_finder.services.economic_import_service.execute_query_single", fake_execute_query_single, ) service = EconomicImportService() invoice_id = service._persist_invoice( 3, { "bookedInvoiceNumber": 123, "customer": {"customerNumber": 10, "name": "ACME"}, "date": "2026-07-01", "grossAmount": 100, }, ) assert invoice_id == 11 assert json.loads(captured["invoice_raw"])["bookedInvoiceNumber"] == 123 def test_economic_import_service_defaults_missing_line_amounts(monkeypatch): from app.modules.invoice_error_finder.services.economic_import_service import EconomicImportService captured = [] def fake_execute_query(query, params=None): if "INSERT INTO invoice_error_finder_economic_invoice_lines" in query: captured.append(params) return [] monkeypatch.setattr( "app.modules.invoice_error_finder.services.economic_import_service.execute_query", fake_execute_query, ) service = EconomicImportService() service._persist_lines( 99, [ { "lineNumber": 1, "description": "Mobilnummer", "quantity": None, "unitNetPrice": None, "totalNetAmount": None, "discountPercentage": None, } ], ) assert len(captured) == 1 assert captured[0][5] == 0.0 assert captured[0][6] == 0.0 assert captured[0][7] == 0.0 assert captured[0][8] == 0.0 def test_detection_service_keeps_customers_without_subscriptions(monkeypatch): from app.modules.invoice_error_finder.services.detection_service import DetectionService def fake_execute_query_single(query, params=None): if "SELECT deleted_at FROM customers" in query: return {"deleted_at": None} return None def fake_execute_query(query, params=None): if "FROM sag_subscriptions" in query: return [] return [] monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query", fake_execute_query, ) service = DetectionService() assert service._is_customer_closed_or_cancelled(55, __import__("datetime").date(2026, 7, 1)) is False def test_detection_service_reopens_expired_ignored_issue(monkeypatch): from datetime import date from app.modules.invoice_error_finder.services.detection_service import DetectionService updates = [] def fake_execute_query_single(query, params=None): if "FROM invoice_error_finder_issues" in query and "SELECT id, status" in query: return {"id": 9, "status": "ignored"} if "SELECT ignored_until FROM invoice_error_finder_issues" in query: return {"ignored_until": date(2026, 7, 1)} return None def fake_execute_query(query, params=None): if "UPDATE invoice_error_finder_issues" in query: updates.append((query, params)) return [] monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query", fake_execute_query, ) service = DetectionService() issue_id = service._upsert_issue( issue_type="missing_line", customer_id=77, product_number="P-1", reference_period_start=date(2026, 7, 1), reference_period_end=date(2026, 7, 31), expected_quantity=2, ) assert issue_id == 9 assert updates assert "ignored_until = NULL" in updates[0][0] def test_detection_service_ignores_non_hub_customer_keys(monkeypatch): from app.modules.invoice_error_finder.services.detection_service import DetectionService def fake_execute_query_single(query, params=None): if "SELECT id FROM customers" in query: return None return None monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query_single", fake_execute_query_single, ) service = DetectionService() customer_id = service._resolve_hub_customer_id( { "customer_key": 56283338, "hub_customer_id": None, } ) assert customer_id is None def test_detection_service_upsert_nulls_unknown_customer_id(monkeypatch): from app.modules.invoice_error_finder.services.detection_service import DetectionService inserted = {} def fake_execute_query_single(query, params=None): if "SELECT id, status" in query and "FROM invoice_error_finder_issues" in query: return None if "SELECT id FROM customers" in query: return None if "INSERT INTO invoice_error_finder_issues" in query: inserted["params"] = params return {"id": 21} return None monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query_single", fake_execute_query_single, ) service = DetectionService() issue_id = service._upsert_issue( issue_type="missing_line", customer_id=702222153, customer_name="Unknown Mapping", product_number="INET-1000", reference_period_start=date(2026, 7, 1), reference_period_end=date(2026, 7, 31), ) assert issue_id == 21 assert inserted["params"][2] is None def test_detection_service_analyze_sweeps_historical_months(monkeypatch): from app.modules.invoice_error_finder.services.detection_service import DetectionService scanned_months = [] current_month = date.today().replace(day=1) first_month = current_month - __import__("dateutil.relativedelta").relativedelta.relativedelta(months=3) def fake_execute_query_single(query, params=None): if "MIN(invoice_date)" in query and "MAX(invoice_date)" in query: return { "first_month": first_month, "last_month": current_month, } return None def fake_analyze_single_month(self, month): scanned_months.append(month) return { "missing_line": 1, "open_order_not_invoiced": 0, "quantity_drop": 0, "price_change": 0, } monkeypatch.setattr( "app.modules.invoice_error_finder.services.detection_service.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( DetectionService, "_analyze_single_month", fake_analyze_single_month, ) service = DetectionService() counts = service.analyze() assert scanned_months == [ first_month + __import__("dateutil.relativedelta").relativedelta.relativedelta(months=1), first_month + __import__("dateutil.relativedelta").relativedelta.relativedelta(months=2), current_month, ] assert counts["missing_line"] == 3 def test_list_issues_supports_unassigned_filter(monkeypatch): from app.modules.invoice_error_finder.backend.router import list_issues captured = {} def fake_execute_query_single(query, params=None): captured["count_query"] = query return {"c": 0} def fake_execute_query(query, params=None): captured["list_query"] = query return [] monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query", fake_execute_query, ) payload = asyncio.run( list_issues( assigned_user_id="null", current_user={}, ) ) assert payload["total"] == 0 assert "assigned_user_id IS NULL" in captured["count_query"] assert "assigned_user_id IS NULL" in captured["list_query"] def test_list_issues_returns_resolved_product_name(monkeypatch): from app.modules.invoice_error_finder.backend.router import list_issues def fake_execute_query_single(query, params=None): if "COUNT(*) AS c" in query: return {"c": 1} return None def fake_execute_query(query, params=None): return [ { "id": 44, "customer_name": "Karise Anlæg & Byg A/S", "product_number": "PRO563", "product_name": None, "resolved_product_name": "Fiberforbindelse 1/1 Gbit.", "status": "open", "issue_type": "missing_line", } ] monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query", fake_execute_query, ) payload = asyncio.run( list_issues( status=None, issue_type=None, customer_id=None, assigned_user_id=None, limit=100, offset=0, current_user={}, ) ) assert payload["items"][0]["resolved_product_name"] == "Fiberforbindelse 1/1 Gbit." def test_get_issue_invoice_history_returns_reference_window(monkeypatch): from app.modules.invoice_error_finder.backend.router import get_issue_invoice_history def fake_execute_query_single(query, params=None): if "FROM invoice_error_finder_issues" in query: return { "id": 14, "customer_id": 77, "customer_name": "ACME", "product_number": "INET-1000", "product_name": "1/1 Gbit Internet", "reference_period_start": date(2026, 7, 1), "reference_period_end": date(2026, 7, 31), } if "FROM customers WHERE id = %s" in query: return { "id": 77, "name": "ACME", "economic_customer_number": 56283338, } return None def fake_execute_query(query, params=None): if "FROM month_window mw" in query: return [ { "month_start": date(2026, 6, 1), "line_count": 1, "total_quantity": 1, "total_amount": 999.0, "invoice_numbers": ["18912"], "invoice_dates": ["2026-06-03"], "descriptions": ["1/1 Gbit Internet"], "is_reference_month": False, }, { "month_start": date(2026, 7, 1), "line_count": 0, "total_quantity": 0, "total_amount": 0, "invoice_numbers": [], "invoice_dates": [], "descriptions": [], "is_reference_month": True, }, { "month_start": date(2026, 8, 1), "line_count": 1, "total_quantity": 1, "total_amount": 999.0, "invoice_numbers": ["19001"], "invoice_dates": ["2026-08-04"], "descriptions": ["1/1 Gbit Internet"], "is_reference_month": False, }, ] if "WITH ranked_invoices AS" in query: return [ { "month_start": date(2026, 6, 1), "invoice_id": 101, "source_invoice_number": "18912", "invoice_date": date(2026, 6, 3), "total_amount": 1248.75, "net_amount": 999.0, "vat_amount": 249.75, "currency": "DKK", "source_type": "booked", "heading": "Periode June 2026", "note_text": "Kundeperiode juni\nEkstra note", "line_number": 1, "product_number": "INET-1000", "product_name": None, "description": "1/1 Gbit Internet", "quantity": 1, "unit_price": 999.0, "line_net_amount": 999.0, }, { "month_start": date(2026, 6, 1), "invoice_id": 101, "source_invoice_number": "18912", "invoice_date": date(2026, 6, 3), "total_amount": 1248.75, "net_amount": 999.0, "vat_amount": 249.75, "currency": "DKK", "source_type": "booked", "heading": "Periode June 2026", "note_text": "Kundeperiode juni\nEkstra note", "line_number": 2, "product_number": "RTR-1", "product_name": None, "description": "Leje af router", "quantity": 1, "unit_price": 249.0, "line_net_amount": 249.0, }, { "month_start": date(2026, 8, 1), "invoice_id": 102, "source_invoice_number": "19001", "invoice_date": date(2026, 8, 4), "total_amount": 1248.75, "net_amount": 999.0, "vat_amount": 249.75, "currency": "DKK", "source_type": "booked", "heading": "Periode August 2026", "note_text": None, "line_number": 1, "product_number": "INET-1000", "product_name": None, "description": "1/1 Gbit Internet", "quantity": 1, "unit_price": 999.0, "line_net_amount": 999.0, }, ] return [] monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query", fake_execute_query, ) payload = asyncio.run(get_issue_invoice_history(14, current_user={})) assert payload["customer_name"] == "ACME" assert payload["product_number"] == "INET-1000" assert len(payload["months"]) == 3 assert payload["months"][1]["is_reference_month"] is True assert payload["months"][1]["line_count"] == 0 assert payload["months"][0]["invoices"][0]["invoice_number"] == "18912" assert payload["months"][0]["invoices"][0]["note_text"] == "Kundeperiode juni\nEkstra note" assert len(payload["months"][0]["invoices"][0]["lines"]) == 2 def test_get_issue_invoice_history_deduplicates_same_invoice_number(monkeypatch): from app.modules.invoice_error_finder.backend.router import get_issue_invoice_history def fake_execute_query_single(query, params=None): if "FROM invoice_error_finder_issues" in query: return { "id": 15, "customer_id": 77, "customer_name": "ACME", "product_number": "INET-1000", "product_name": "1/1 Gbit Internet", "reference_period_start": date(2026, 7, 1), "reference_period_end": date(2026, 7, 31), } if "FROM customers WHERE id = %s" in query: return { "id": 77, "name": "ACME", "economic_customer_number": 56283338, } return None def fake_execute_query(query, params=None): if "FROM month_window mw" in query: return [ { "month_start": date(2026, 7, 1), "line_count": 2, "total_quantity": 2, "total_amount": 1444.0, "invoice_numbers": ["20098", "20098"], "invoice_dates": ["2026-03-13", "2026-03-13"], "descriptions": ["Fiberforbindelse", "Fiberforbindelse"], "is_reference_month": True, }, ] if "WITH ranked_invoices AS" in query: return [ { "month_start": date(2026, 7, 1), "invoice_id": 201, "source_invoice_number": "20098", "invoice_date": date(2026, 3, 13), "total_amount": 902.5, "net_amount": 722.0, "vat_amount": 180.5, "currency": "DKK", "source_type": "paid", "heading": None, "note_text": None, "line_number": 1, "product_number": "INET-1000", "product_name": None, "description": "Fiberforbindelse", "quantity": 1, "unit_price": 722.0, "line_net_amount": 722.0, } ] return [] monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query_single", fake_execute_query_single, ) monkeypatch.setattr( "app.modules.invoice_error_finder.backend.router.execute_query", fake_execute_query, ) payload = asyncio.run(get_issue_invoice_history(15, current_user={})) assert len(payload["months"][0]["invoices"]) == 1 assert payload["months"][0]["invoices"][0]["source_type"] == "paid"