Fix Jasmin customer key and amount mapping

This commit is contained in:
plx
2026-07-29 14:27:17 +00:00
parent 0c5ee4e581
commit bbfadfa0fd
3 changed files with 240 additions and 2 deletions

View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Repair a Jasmin reconciliation item affected by incorrect customer mapping.
Dry-run is the default. Use --apply only after reviewing the printed plan.
This script does not create/link opportunities and does not resolve the item.
"""
from __future__ import annotations
import argparse
import json
import sys
from decimal import Decimal, InvalidOperation
from pathlib import Path
from sqlalchemy import text
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.db import engine
def _clean(value: object) -> str:
return str(value or "").strip()
def _money(record: dict) -> Decimal | None:
values = [
record.get("payableAmountAmount"),
(record.get("payableAmount") or {}).get("amount")
if isinstance(record.get("payableAmount"), dict)
else record.get("payableAmount"),
record.get("grossValueAmount"),
]
for value in values:
if value in (None, ""):
continue
try:
return Decimal(str(value)).quantize(Decimal("0.01"))
except (InvalidOperation, ValueError):
continue
return None
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("document_number", help="Ex.: ORC.ORC2026.136")
parser.add_argument("--apply", action="store_true", help="Aplicar a reparação; sem esta flag faz dry-run")
args = parser.parse_args()
with engine.begin() as conn:
item = conn.execute(
text("""
SELECT ri.id::text, ri.status, ri.opportunity_id::text,
ri.customer_id::text, ri.customer_tax_id,
ri.document_number, ri.amount, ri.payload,
c.name AS customer_name, c.tax_id,
c.jasmin_customer_party_key
FROM reconciliation_items ri
LEFT JOIN customers c ON c.id = ri.customer_id
WHERE ri.document_number = :document_number
FOR UPDATE OF ri
"""),
{"document_number": args.document_number},
).mappings().first()
if not item:
raise SystemExit(f"Documento não encontrado: {args.document_number}")
if not item["customer_id"]:
raise SystemExit("O item não tem customer_id; reparação automática recusada")
payload = item["payload"] or {}
record = payload.get("record") if isinstance(payload, dict) else None
if not isinstance(record, dict):
raise SystemExit("payload.record não existe ou não é um objeto")
party_key = _clean(record.get("buyerCustomerParty") or record.get("accountingParty"))
payload_tax_id = _clean(record.get("buyerCustomerPartyTaxId") or record.get("accountingPartyTaxId"))
amount = _money(record)
if not party_key:
raise SystemExit("Não foi possível obter buyerCustomerParty/accountingParty")
if payload_tax_id and _clean(item["tax_id"]) and payload_tax_id != _clean(item["tax_id"]):
raise SystemExit(
f"NIF divergente: cliente={item['tax_id']} payload={payload_tax_id}; reparação recusada"
)
conflict = conn.execute(
text("""
SELECT id::text, name, tax_id
FROM customers
WHERE jasmin_customer_party_key = :party_key
AND id <> CAST(:customer_id AS UUID)
LIMIT 1
"""),
{"party_key": party_key, "customer_id": item["customer_id"]},
).mappings().first()
if conflict:
raise SystemExit(
"Party key já pertence a outro cliente: "
+ json.dumps(dict(conflict), ensure_ascii=False, default=str)
)
plan = {
"mode": "apply" if args.apply else "dry-run",
"document_number": item["document_number"],
"reconciliation_item_id": item["id"],
"customer_id": item["customer_id"],
"customer_name": item["customer_name"],
"tax_id": item["tax_id"],
"party_key_before": item["jasmin_customer_party_key"],
"party_key_after": party_key,
"amount_before": str(item["amount"]) if item["amount"] is not None else None,
"amount_after": str(amount) if amount is not None else None,
"status_unchanged": item["status"],
"opportunity_id_unchanged": item["opportunity_id"],
}
print(json.dumps(plan, ensure_ascii=False, indent=2, default=str))
if not args.apply:
conn.rollback()
print("DRY-RUN: nenhuma alteração aplicada.")
return 0
conn.execute(
text("""
UPDATE customers
SET jasmin_customer_party_key = :party_key,
metadata = jsonb_set(
COALESCE(metadata, '{}'::jsonb),
'{external_customer_key}',
to_jsonb(CAST(:party_key AS text)),
true
),
updated_at = now()
WHERE id = CAST(:customer_id AS UUID)
"""),
{"party_key": party_key, "customer_id": item["customer_id"]},
)
if amount is not None:
conn.execute(
text("""
UPDATE reconciliation_items
SET amount = :amount,
updated_at = now()
WHERE id = CAST(:item_id AS UUID)
"""),
{"amount": amount, "item_id": item["id"]},
)
print("Reparação aplicada. O item permanece aberto e sem opportunity_id.")
return 0
if __name__ == "__main__":
raise SystemExit(main())