""" Phase B: multi-marketplace parsing, localization, and classification. Fast tests use synthetic localized files; the 13-marketplace reconciliation against the Jan-2026 AR workbook benchmarks runs as an integration test (needs the real files in "/Amazon Transactions reports Jan-2026"). """ from __future__ import annotations from datetime import date from pathlib import Path import openpyxl import pytest from app.core.dates import parse_amazon_date_fast from app.core.i18n import ( currency_for_region, default_fx_for_region, is_storage_like, normalize_type, ) from app.core.column_map import build_mapping, normalize_header from app.core.pipeline import process from .conftest import SAMPLE_DIR # --------------------------------------------------------------------- dates @pytest.mark.parametrize("text,expected", [ ("Jan 15, 2026 5:45:40 PM PST", date(2026, 1, 15)), # USA ("Jan 1, 2026 12:03:09 a.m. PST", date(2026, 1, 1)), # Canada a.m. ("1 Jan 2026 00:00:06 UTC", date(2026, 1, 1)), # UK day-first ("31 Dec 2025 10:35:21 pm GMT+9", date(2025, 12, 31)), # Australia ("31 déc. 2025 23:00:16 UTC", date(2025, 12, 31)), # France ("31.12.2025 23:00:01 UTC", date(2025, 12, 31)), # Germany dotted ("31 dic 2025 23:00:10 UTC", date(2025, 12, 31)), # Italy/Spain ("31 dec 2025 23:04:31 UTC", date(2025, 12, 31)), # Netherlands ("1 sty 2026 00:20:40 UTC", date(2026, 1, 1)), # Poland ("1 jan. 2026 03:28:09 UTC", date(2026, 1, 1)), # Sweden ("15 Oca 2026 09:00:00 UTC", date(2026, 1, 15)), # Turkey ]) def test_localized_dates(text, expected): assert parse_amazon_date_fast(text) == expected # --------------------------------------------------------------------- types @pytest.mark.parametrize("raw,canonical", [ ("Commande", "Order"), ("Bestellung", "Order"), ("Ordine", "Order"), ("Pedido", "Order"), ("Bestelling", "Order"), ("Zamówienie", "Order"), ("Remboursement", "Refund"), ("Erstattung", "Refund"), ("Zwrot kosztów", "Refund"), ("Übertrag", "Transfer"), ("Transfert", "Transfer"), ("Transférer", "Transfer"), ("Przelew", "Transfer"), ("Överföring", "Transfer"), ("Overboeking", "Transfer"), ("Versand durch Amazon Lagergebühr", "FBA Inventory Fee"), ("Costo di stoccaggio Logistica di Amazon", "FBA Inventory Fee"), ("Lageravgift för FBA", "FBA Inventory Fee"), ("Fulfilment by Amazon inventory fee", "FBA Inventory Fee"), ("Delivery Services", "Shipping Services"), ("Gebühren für Kundenrücksendungen mit Versand durch Amazon", "FBA Customer Return Fee"), ("SomeBrandNewType", "SomeBrandNewType"), # unknown passes through ]) def test_type_normalization(raw, canonical): assert normalize_type(raw) == canonical def test_storage_detection(): assert is_storage_like("FBA Inventory Fee", None) assert is_storage_like("Service Fee", "Monthly storage fee for December") assert is_storage_like("Servicegebühr", "Langzeitlagergebühr Dezember") assert not is_storage_like("Order", "Utopia Bedding duvet") def test_currency_and_fx_config(): assert currency_for_region("Canada") == "CAD" assert currency_for_region("Poland") == "PLN" assert currency_for_region("Germany") == "EUR" assert default_fx_for_region("USA") == 1.0 assert default_fx_for_region("UK") == pytest.approx(1.368908) # ------------------------------------------------------------ localized headers GERMAN_HEADERS = [ "Datum/Uhrzeit", "Abrechnungsnummer", "Typ", "Bestellnummer", "SKU", "Beschreibung", "Menge", "Marketplace", "Versand", "Ort der Bestellung", "Bundesland", "Postleitzahl", "Steuererhebungsmodell", "Umsätze", "Produktumsatzsteuer", "Gutschrift für Versandkosten", "Steuer auf Versandgutschrift", "Gutschrift für Geschenkverpackung", "Steuer auf Geschenkverpackungsgutschriften", "Rabatte aus Werbeaktionen", "Steuer auf Aktionsrabatte", "Einbehaltene Steuer auf Marketplace", "Verkaufsgebühren", "Gebühren zu Versand durch Amazon", "Andere Transaktionsgebühren", "Andere", "Gesamt", ] def test_german_headers_map_fully(): cells = [(chr(65 + i) if i < 26 else "A" + chr(65 + i - 26), h) for i, h in enumerate(GERMAN_HEADERS)] m = build_mapping(cells, header_row=8) assert m.is_valid assert not m.unmapped, m.unmapped assert m.field_to_col["settlement_id"] == "B" assert m.field_to_col["total"] == "AA" def test_saved_rule_overrides(): m = build_mapping([("A", "date/time"), ("B", "settlement id"), ("C", "type"), ("D", "Weird New Fee Column"), ("E", "total")], 8, saved_overrides={normalize_header("Weird New Fee Column"): "other"}) assert m.field_to_col["other"] == "D" assert not m.unmapped # -------------------------------------------------- synthetic localized file (Dutch-style) def _make_dutch_file(path: str) -> None: """Compact EU schema: localized header row 8, helper translation row 9, type-2 column.""" wb = openpyxl.Workbook() ws = wb.active ws.title = "Nederland Transacties" for i in range(1, 8): ws.append([f"preambule {i}"]) ws.append(["datum/tijd", "schikkings-ID", "type", None, "bestelnummer", "sku", "beschrijving", "aantal", "marketplace", "fulfillment", "verkoop van producten", "geïnde omzetbelasting", "Belasting voor marketplace-facilitator", "verkoopkosten", "fba-vergoedingen", "overige", "totaal"]) # Finance helper/translation row (must be skipped) ws.append(["datum/tijd", "schikkings-ID", "type", "type 2", "bestelnummer", "sku", "beschrijving", "aantal", "marketplace", "fulfillment", "sales", "sales tax", "facilitator", "cost", "fba", "other", "total"]) rows = [ # paid settlement 9100 (transfer tagged 9200, received Jan 6) ["2 jan. 2026 10:00:00 UTC", 9100, "Bestelling", "Order", "b-1", "SKU1", "d", 1, "amazon.nl", "Amazon", 100.0, 21.0, -21.0, -10.0, -20.0, 0.0, 70.0], ["6 jan. 2026 09:00:00 UTC", 9200, "Overboeking", "Transfer", None, None, "Naar bankrekening", None, None, None, 0.0, 0.0, 0.0, 0.0, 0.0, -70.0, -70.0], # receivable settlement 9200 ["15 jan. 2026 10:00:00 UTC", 9200, "Bestelling", "Order", "b-2", "SKU2", "d", 1, "amazon.nl", "Amazon", 50.0, 10.5, -10.5, -5.0, -8.0, 0.0, 37.0], ["20 jan. 2026 10:00:00 UTC", 9200, "Terugbetaling", "Refund", "b-3", "SKU2", "d", 1, "amazon.nl", "Amazon", -10.0, -2.1, 2.1, 1.0, 0.0, 0.0, -9.0], # storage fee under Service type with storage description -> potential storage ["21 jan. 2026 10:00:00 UTC", 9200, "Servicekosten", "Service charge", None, None, "FBA opslag kosten januari", None, "amazon.nl", None, 0.0, 0.0, 0.0, 0.0, 0.0, -3.0, -3.0], ] for r in rows: ws.append(r) wb.save(path) @pytest.fixture() def dutch_file(tmp_path: Path) -> str: p = tmp_path / "Netherlands Amazon Transactions January, 2026.xlsx" _make_dutch_file(str(p)) return str(p) def test_localized_file_end_to_end(dutch_file): res = process([dutch_file], month_end=date(2026, 1, 31), clearing_lag_days=2) meta = res.file_metas[0] assert meta.helper_rows_skipped == 1 # translation row skipped assert not meta.missing_required assert meta.imported_rows == 5 assert meta.currency == "EUR" nl = res.receivable.marketplaces["Netherlands"] # receivable = settlement 200 non-transfer rows: 37.0 - 9.0 - 3.0 = 25.0 assert nl.accounts["All Orders"].additional_sales == pytest.approx(25.0) # settlement 9100 paid (transfer tagged 9200 received Jan 6) st100 = res.aggregation.settlements[("Netherlands", "All Orders", "9100")] assert st100.status == "paid" # potential storage fee flagged (Service type, storage-like description) assert res.aggregation.potential_storage_by_mkt.get("Netherlands") == pytest.approx(-3.0) # ---------------------------------------------------- 13-market benchmark (integration) BENCHMARKS = { "Canada": 1103089.77, "UK": 747126.05, "Australia": 79.41, "Italy": 209902.12, "France": 279586.75, "Spain": 298421.23, "Netherlands": 20967.72, "Germany": 487133.16, "Belgium": 15036.66, "Ireland": 9348.13, "Poland": 18059.37, "Turkey": 0.0, "Sweden": 39624.00, } @pytest.mark.integration def test_all_marketplaces_reconcile_to_workbook(): folder = SAMPLE_DIR / "Amazon Transactions reports Jan-2026" files = sorted(str(p) for p in folder.glob("*.xlsx")) if len(files) < 13: pytest.skip(f"marketplace files not found in {folder}") res = process(files, month_end=date(2026, 1, 31), clearing_lag_days=2) for mkt, bench in BENCHMARKS.items(): m = res.receivable.marketplaces.get(mkt) got = m.additional_sales if m else 0.0 assert got == pytest.approx(bench, abs=0.02), f"{mkt}: {got} != {bench}"