191 lines
8.9 KiB
Python
191 lines
8.9 KiB
Python
"""
|
|
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
|
|
"<samples>/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}"
|