""" Month-end controls. Why this module exists ---------------------- The close previously reported "Reconciled" from a single identity: uploaded_total == receivable_orders + paid_orders + transfers_total That identity cannot fail. `uploaded_total` is accumulated from the same parsed record stream that fills the three buckets, and every record lands in exactly one of them, so the two sides are the same sum written twice. It reported "Reconciled" on the Jan-2026 close while the Reconciliation Control was understating the group receivable by USD 444,658.44, and it would report "Reconciled" just as happily if an entire marketplace file failed to parse — because a file that yields no rows contributes zero to *both* sides. Every control below is instead **independent of the thing it checks**: it compares the engine's output against something the engine did not produce (the worksheet's own declared extent, the source `total` column, the FX rates a human confirmed, a second calculation method). A control that cannot fail is not a control. Severity `error` blocks the close: no receivable figure is released to the dashboard or to an export until it is resolved. A number that cannot be trusted is never shown — a missing number cannot be posted to the ledger, a wrong one can. """ from __future__ import annotations from dataclasses import dataclass, field # Roll-forward vs settlement method may legitimately differ by rounding across marketplaces. DUAL_METHOD_TOLERANCE = 1.0 # Σ(journal lines) vs Σ(total): float accumulation over millions of rows, not a real gap. COMPONENT_TOLERANCE = 0.05 PASS, FAIL, NA = "pass", "fail", "not_applicable" @dataclass class ControlResult: key: str label: str status: str = PASS severity: str = "error" detail: str = "" evidence: list[str] = field(default_factory=list) @property def blocking(self) -> bool: return self.status == FAIL and self.severity == "error" def _ok(key: str, label: str, detail: str) -> ControlResult: return ControlResult(key=key, label=label, status=PASS, detail=detail) def _na(key: str, label: str, detail: str) -> ControlResult: return ControlResult(key=key, label=label, status=NA, severity="info", detail=detail) def _fail(key: str, label: str, detail: str, evidence: list[str], severity: str = "error") -> ControlResult: return ControlResult(key=key, label=label, status=FAIL, severity=severity, detail=detail, evidence=evidence[:10]) # --------------------------------------------------------------------------- C1 def c1_source_row_count(file_metas) -> ControlResult: """ Every data row the worksheet declares must be either imported or deliberately skipped. Independent because the expected count comes from the worksheet's own , read from the file header — not from our row stream. This is the control that distinguishes "this marketplace had no transactions" from "this marketplace's file failed to parse", which otherwise produce an identical, silent zero. """ key, label = "C1", "Source row count" checked, unknown, bad = 0, [], [] for m in file_metas: if not getattr(m, "sheet_last_row", 0): unknown.append(m.filename) continue checked += 1 expected = m.expected_data_rows got = m.rows_accounted_for if got != expected: bad.append( f"{m.filename}: worksheet declares {expected:,} data rows " f"(rows 1..{m.sheet_last_row}, header row {m.header_row}) but " f"{got:,} were accounted for " f"(imported {m.imported_rows:,}, helper {m.helper_rows_skipped:,}, " f"blank {m.blank_rows_skipped:,}) — {expected - got:+,} unexplained") if bad: return _fail(key, label, f"{len(bad)} file(s) consumed a different number of rows than the " f"worksheet declares. Rows may have been dropped during parsing.", bad) if not checked: return _na(key, label, "No worksheet declared its extent; row counts unverifiable.") note = f"{checked} file(s) fully accounted for." if unknown: note += f" {len(unknown)} file(s) declared no extent: {', '.join(unknown[:3])}." return _ok(key, label, note) # --------------------------------------------------------------------------- C2 def c2_column_completeness(journal_payload: dict, uploaded_total: float) -> ControlResult: """ Every amount column must be accounted for: Σ(journal GL lines) == Σ(`total` column). Independent because the journal is built by summing the individual component columns (product sales, fees, tax, …) while `total` is Amazon's own pre-computed column AD. If a column is unmapped, mapped twice, or a new column appears, the two sides separate. This is what catches an Australia-style `fulfilment by amazon fees` alias gap, and unlike the old identity it is a genuine cross-check between two different numbers. """ key, label = "C2", "Column completeness" if not journal_payload or not journal_payload.get("lines"): return _na(key, label, "No journal decomposition available.") per_market = journal_payload.get("per_marketplace") or { journal_payload.get("marketplace", "USA"): journal_payload} bad, total_lines = [], 0.0 for mkt, jr in per_market.items(): s = sum(l.get("total", 0.0) for l in jr.get("lines", [])) total_lines += s delta = round(uploaded_total - total_lines, 2) if abs(delta) > COMPONENT_TOLERANCE: for mkt, jr in per_market.items(): s = sum(l.get("total", 0.0) for l in jr.get("lines", [])) bad.append(f"{mkt}: GL lines sum to {s:,.2f}") return _fail(key, label, f"Sum of GL lines {total_lines:,.2f} != sum of the source `total` column " f"{uploaded_total:,.2f} (difference {delta:,.2f}). An amount column is " f"unmapped, mapped twice, or newly added by Amazon.", bad) return _ok(key, label, f"GL lines reconcile to the source `total` column " f"({total_lines:,.2f}, difference {delta:,.2f}).") # --------------------------------------------------------------------------- C3 def c3_bucket_completeness(agg) -> ControlResult: """ Every money-carrying order row must be placeable in a receivable bucket. A row whose `account type` is unrecognized fails the receivable SUMIFS filter; a row whose settlement id is not numeric sorts below every boundary and is classified "paid". Both silently REDUCE the receivable, and neither previously raised anything. """ key, label = "C3", "Bucket completeness" problems, evidence = [], [] if agg.unclassified_acct_count: problems.append( f"{agg.unclassified_acct_count:,} row(s) carrying " f"{agg.unclassified_acct_total:,.2f} have an unrecognized account type") evidence += agg.unclassified_acct_samples if agg.unclassified_sid_count: problems.append( f"{agg.unclassified_sid_count:,} row(s) carrying " f"{agg.unclassified_sid_total:,.2f} have a blank or non-numeric settlement id") evidence += agg.unclassified_sid_samples if problems: return _fail(key, label, "; ".join(problems) + ". These are excluded from the receivable — " "map or correct them before relying on this close.", evidence) return _ok(key, label, "Every order row resolves to a receivable bucket.") # --------------------------------------------------------------------------- C4 def c4_dual_method(rows: list[dict], openings_all_zero: bool = False) -> ControlResult: """ The two independent closing methods must agree, per marketplace, in LOCAL currency: settlement method : ROUND(reserve + additional sales) roll-forward : opening AR balance + net revenue - payouts received Independent because they share no arithmetic — one filters open settlements, the other accumulates GL movement off an opening balance. A gap means the opening balance, a reserve, or a timing item is wrong. Reported as a warning rather than a block, because the opening balance is a Finance input the engine cannot derive: until it is entered (or carried forward from the prior closing) the roll-forward is measuring only this month's movement and MUST differ. That case is called out explicitly — "no openings entered yet" is a different situation from "openings are entered and the two methods still disagree", and only the second is a real problem. """ key, label = "C4", "Dual-method agreement" if not rows: return _na(key, label, "No per-marketplace movement available.") bad = [] for r in rows: s, c = r.get("settlement_closing"), r.get("closing_local") if s is None or c is None: continue d = round(c - s, 2) if abs(d) > DUAL_METHOD_TOLERANCE: bad.append(f"{r['marketplace']}: roll-forward {c:,.2f} vs settlement {s:,.2f} " f"({r.get('currency', '')}) — difference {d:,.2f}") if bad: if openings_all_zero: detail = (f"Every opening AR balance on this closing is zero, so the roll-forward " f"({len(bad)} marketplace(s) differing) is only measuring this month's " f"movement — not the receivable actually outstanding. The settlement " f"figure is the reliable one until opening balances are entered. " f"Carry them forward from the prior closing, or enter them from last " f"month's workbook, on the Opening Balances tab.") else: detail = (f"{len(bad)} marketplace(s) disagree between the two closing methods " f"even with opening balances entered. Review the opening balance, the " f"reserve, or a month-boundary timing item for each one below.") return _fail(key, label, detail, bad, severity="warning") return _ok(key, label, f"Both methods agree across {len(rows)} marketplace(s).") # --------------------------------------------------------------------------- C5 def c5_fx_confirmed(fx_rows, reporting_month: str, markets: list[str]) -> ControlResult: """ Every non-USD marketplace must have an FX rate a human confirmed FOR THIS MONTH. The seeded `DEFAULT_FX_USD` table is a January-2026 snapshot. Left as a default it would value a July close at January's rates and say nothing, so a seeded-but-unconfirmed rate counts as missing. """ key, label = "C5", "FX rates confirmed" by_mkt = {r.marketplace: r for r in fx_rows} missing = [] for mkt in markets: row = by_mkt.get(mkt) if row is None: missing.append(f"{mkt}: no FX rate recorded") continue if (row.currency or "USD").upper() == "USD" and float(row.rate or 1.0) == 1.0: continue # USD at parity needs no confirmation if not row.confirmed_by: missing.append(f"{mkt}: rate {row.rate} ({row.currency}) is a seeded default " f"[{row.source or 'unknown source'}] — not confirmed by anyone") elif (row.confirmed_month or "") != reporting_month: missing.append(f"{mkt}: rate {row.rate} ({row.currency}) was confirmed for " f"{row.confirmed_month or 'an unknown month'}, not {reporting_month}") if missing: return _fail(key, label, f"{len(missing)} marketplace(s) have no FX rate confirmed for " f"{reporting_month}. Converted totals would use stale rates.", missing) return _ok(key, label, f"FX confirmed for {reporting_month} across all marketplaces.") # --------------------------------------------------------------------------- C6 def c6_currency_integrity(control_total_usd: float | None, all_markets_total_usd: float | None) -> ControlResult: """ Two surfaces roll the marketplaces up to a group total; they must agree to the cent. Independent because they are separate code paths over the same data. Before the fix the Reconciliation Control added the local-currency closings together (USD + EUR + GBP + PLN + SEK + CAD + AUD as one figure) while the All-Markets tab converted properly — a USD 444,658.44 gap on Jan-2026, on the figure that gates sign-off. """ key, label = "C6", "Currency integrity" if control_total_usd is None or all_markets_total_usd is None: return _na(key, label, "Group totals not available.") d = round(control_total_usd - all_markets_total_usd, 2) if abs(d) > 0.01: return _fail(key, label, f"Group closing receivable differs between surfaces: Reconciliation " f"Control {control_total_usd:,.2f} vs All Markets " f"{all_markets_total_usd:,.2f} (difference {d:,.2f} USD). One of them is " f"adding currencies without converting.", [f"difference {d:,.2f} USD"]) return _ok(key, label, f"Both group roll-ups agree at {control_total_usd:,.2f} USD.") def blocking_summary(results: list[ControlResult]) -> str: """One-line reason a close is blocked, or "" when nothing blocks.""" blockers = [r for r in results if r.blocking] if not blockers: return "" return " | ".join(f"{r.key} {r.label}: {r.detail}" for r in blockers)