"""Generate the dry-run ERP write plan artifact for the 20-product sample. Calls the running fiscal-hub /v1/classify/batch (read-only classification, no Firebird access, no dataset creation) and turns each decision into field-level write proposals via central.app.dry_run_planner. Never issues an ERP write. """ import argparse import json import sys import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "central")) from app.dry_run_planner import build_field_proposals, mva_evidence, split_by_eligibility # noqa: E402 def classify_batch(hub_url, token, products): payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"]} for p in products]} request = urllib.request.Request( f"{hub_url}/v1/classify/batch", data=json.dumps(payload, ensure_ascii=True).encode(), headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"}, method="POST", ) with urllib.request.urlopen(request) as response: return json.load(response)["results"] def main(): parser = argparse.ArgumentParser() parser.add_argument("--sample", default=str(ROOT / "docs" / "SAMPLE_PRODUCTS_V1.json")) parser.add_argument("--hub-url", default="http://localhost:8000") parser.add_argument("--token", required=True) parser.add_argument("--output", default=str(ROOT / "docs" / "DRY_RUN_PLAN_V1.json")) parser.add_argument("--artifact-name", default="DRY_RUN_PLAN_V1") parser.add_argument("--firebird-current", default=None, help="JSON file with fresh CODIGO/IPI/CEST read from Firebird (read-only). Omit to keep current values unknown.") args = parser.parse_args() sample = json.loads(Path(args.sample).read_text(encoding="utf-8")) products = sample["sample_without_ncm"] decisions = classify_batch(args.hub_url, args.token, products) fresh_by_code = {} if args.firebird_current: for row in json.loads(Path(args.firebird_current).read_text(encoding="utf-8")): fresh_by_code[row["CODIGO"]] = row field_proposals, mva_records = [], [] for product, decision in zip(products, decisions): code = product["CODIGO"] fresh = fresh_by_code.get(code) if fresh is not None: current = {"ipi": fresh.get("IPI"), "cest": fresh.get("CEST"), "ex_tipi": fresh.get("EX_TIPI")} else: current = {} if "CEST" in product: current["cest"] = product["CEST"] field_proposals.extend(build_field_proposals(code, decision, current)) mva_records.append(mva_evidence(code, decision)) split = split_by_eligibility(field_proposals) ipi_rows = [p for p in field_proposals if p["field"] == "IPI"] ex_rows = [p for p in field_proposals if p["field"] == "EX_TIPI"] cest_rows = [p for p in field_proposals if p["field"] == "CEST"] ipi_metrics = { "ipi_numeric_proposals": sum(1 for p in ipi_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")), "ipi_non_numeric_status": sum(1 for p in ipi_rows if p["status"] == "NON_NUMERIC_IPI_STATUS"), "ipi_source_blank": sum(1 for p in ipi_rows if p["status"] == "SOURCE_BLANK"), "ipi_same_value": sum(1 for p in ipi_rows if p["status"] == "SAME_VALUE"), "ipi_change_required": sum(1 for p in ipi_rows if p["status"] == "CHANGE_REQUIRED"), "ipi_write_eligible": sum(1 for p in ipi_rows if p["write_eligible"]), # every emitted EX_TIPI row implies TIPI had at least one EX-specific entry for that NCM. "ex_tipi_db_match_products": len(ex_rows), "ex_tipi_concrete_proposals": sum(1 for p in ex_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")), "ex_tipi_candidates": sum(1 for p in ex_rows if p["status"] == "CANDIDATE"), "ex_tipi_write_eligible": sum(1 for p in ex_rows if p["write_eligible"]), } cest_metrics = { "cest_db_match_products": sum(1 for p in cest_rows if p["status"] != "NO_CEST_MAPPING_FOUND" and p["reason_code"] != "NO_CEST_MAPPING_FOUND_FOR_NCM"), "cest_confirmed": sum(1 for p in cest_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")), "cest_candidates": sum(1 for p in cest_rows if p["status"] == "CANDIDATE"), "cest_unresolved": sum(1 for p in cest_rows if p["status"] == "UNRESOLVED"), "cest_write_eligible": sum(1 for p in cest_rows if p["write_eligible"]), } artifact = { "artifact": args.artifact_name, "firebird_read": "OK" if fresh_by_code else "BLOCKED_EXTERNAL_MISSING_CREDENTIALS", "firebird_writes": 0, "products_planned": len(products), "current_value_source": ( f"live read-only SELECT from C000025 via {args.firebird_current}" if fresh_by_code else "docs/SAMPLE_PRODUCTS_V1.json (CEST) — C000025.IPI has no authorized offline source; FB_USER/FB_PASSWORD unavailable in this environment" ), "field_proposals": field_proposals, "mva_evidence": mva_records, "ipi_metrics": ipi_metrics, "cest_metrics": cest_metrics, "write_eligible_count": len(split["eligible"]), "write_blocked_count": len(split["blocked"]), } Path(args.output).write_text(json.dumps(artifact, indent=2, ensure_ascii=False), encoding="utf-8") print(json.dumps({ "products": len(products), "field_proposals": len(field_proposals), "write_eligible": len(split["eligible"]), "write_blocked": len(split["blocked"]), "output": args.output, }, indent=2)) if __name__ == "__main__": main()