diff --git a/broker_sync/sinks/wealthfolio.py b/broker_sync/sinks/wealthfolio.py index e968927..e69cd73 100644 --- a/broker_sync/sinks/wealthfolio.py +++ b/broker_sync/sinks/wealthfolio.py @@ -221,20 +221,35 @@ class WealthfolioSink: real = await self._request("POST", _IMPORT_REAL, json={"activities": valid_rows}) real.raise_for_status() raw = real.json() + # Two observed response shapes: + # - {activities:[...], importRunId:"...", summary:{total,imported,skipped,...}} + # - bare list (older builds) if isinstance(raw, dict) and "activities" in raw: got = raw["activities"] - assert isinstance(got, list) + summary = raw.get("summary") if isinstance(raw.get("summary"), dict) else None elif isinstance(raw, list): got = raw + summary = None else: got = [] - # Silent-drop detection: if we sent N valid rows but got 0 back, something - # is silently rejecting them (usually a date-format or asset-resolution - # quirk that check() didn't catch). Raise so the pipeline records failure - # instead of marking the rows as synced when they never landed. - if valid_rows and not got: - # Also surface any per-row `errors` or `warnings` from the check step - # — those are often the best hint about why /import dropped them. + summary = None + # Summary.imported is THE truth. The `activities` field echoes input + # with errors annotated — its length equals input even when zero + # actually persisted. + if summary is not None: + imported_n = int(summary.get("imported", 0)) + total_n = int(summary.get("total", len(valid_rows))) + if imported_n < total_n: + err_msg = summary.get("errorMessage") or "no errorMessage" + skipped = int(summary.get("skipped", 0)) + dupes = int(summary.get("duplicates", 0)) + raise ImportValidationError( + f"Wealthfolio /import persisted {imported_n}/{total_n} " + f"(skipped={skipped} duplicates={dupes}). " + f"errorMessage: {err_msg}" + ) + # Legacy silent-drop guard for no-summary responses. + elif valid_rows and not got: first_warn = next( (r.get("warnings") for r in checked if isinstance(r, dict) and r.get("warnings")), None, @@ -242,6 +257,6 @@ class WealthfolioSink: raise ImportValidationError( f"Wealthfolio /import silently dropped all {len(valid_rows)} rows. " f"First checked row: {checked[0] if checked else 'none'}. " - f"First warning (if any): {first_warn}" + f"First warning: {first_warn}" ) return got