74 lines
3.3 KiB
Python
74 lines
3.3 KiB
Python
"""File-coverage validation for a reporting month: wrong month, overlaps, gaps, coverage.
|
|
|
|
Multi-marketplace aware: overlap/gap checks compare only files of the SAME marketplace
|
|
(each market ships its own full-month file), and a few days of boundary spillover is
|
|
tolerated (Amazon exports use UTC/PST cuts, so a January file legitimately starts with
|
|
Dec-31 evening rows).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from .xlsx_reader import FileMeta
|
|
|
|
# Rows this close to the month boundary are normal timezone spillover, not a wrong month.
|
|
BOUNDARY_GRACE_DAYS = 3
|
|
|
|
|
|
def validate_coverage(metas: list[FileMeta], month_end: date | None) -> list[dict]:
|
|
exc: list[dict] = []
|
|
if not month_end:
|
|
return exc
|
|
first = month_end.replace(day=1)
|
|
lo = first - timedelta(days=BOUNDARY_GRACE_DAYS)
|
|
hi = month_end + timedelta(days=BOUNDARY_GRACE_DAYS)
|
|
|
|
dated = [(m.filename, m.min_date, m.max_date, m.marketplace or "")
|
|
for m in metas if m.min_date and m.max_date]
|
|
|
|
# (1) files clearly outside the reporting month — prevents mixing months.
|
|
for fn, mn, mx, _mkt in dated:
|
|
if mx < lo or mn > hi:
|
|
exc.append({"category": "wrong_month", "severity": "error",
|
|
"detail": f"{fn} covers {mn}..{mx}, outside reporting month "
|
|
f"{first:%Y-%m}", "source": fn})
|
|
elif mn < lo or mx > hi:
|
|
exc.append({"category": "wrong_month", "severity": "warning",
|
|
"detail": f"{fn} extends well beyond the reporting month "
|
|
f"({mn}..{mx} vs {first}..{month_end})", "source": fn})
|
|
|
|
# (2)+(3) overlaps and gaps — only within the SAME marketplace.
|
|
by_mkt: dict[str, list[tuple[str, date, date]]] = {}
|
|
for fn, mn, mx, mkt in dated:
|
|
by_mkt.setdefault(mkt, []).append((fn, mn, mx))
|
|
for mkt, entries in by_mkt.items():
|
|
if len(entries) < 2:
|
|
continue
|
|
ds = sorted(entries, key=lambda x: x[1])
|
|
for i in range(1, len(ds)):
|
|
prev, cur = ds[i - 1], ds[i]
|
|
if cur[1] <= prev[2]:
|
|
exc.append({"category": "overlapping_files", "severity": "warning",
|
|
"detail": f"{cur[0]} ({cur[1]}..{cur[2]}) overlaps "
|
|
f"{prev[0]} ({prev[1]}..{prev[2]})", "source": cur[0]})
|
|
else:
|
|
gs, ge = prev[2] + timedelta(days=1), cur[1] - timedelta(days=1)
|
|
if gs <= ge:
|
|
exc.append({"category": "missing_dates", "severity": "warning",
|
|
"detail": f"Missing transactions {gs}..{ge} between "
|
|
f"{prev[0]} and {cur[0]}", "source": ""})
|
|
|
|
# (4) whole-month coverage across all files.
|
|
if dated:
|
|
allmin = min(x[1] for x in dated)
|
|
allmax = max(x[2] for x in dated)
|
|
if allmin > first:
|
|
exc.append({"category": "missing_dates", "severity": "warning",
|
|
"detail": f"No transactions before {allmin}; month starts {first}",
|
|
"source": ""})
|
|
if allmax < month_end:
|
|
exc.append({"category": "missing_dates", "severity": "warning",
|
|
"detail": f"No transactions after {allmax}; month ends {month_end}",
|
|
"source": ""})
|
|
return exc
|