bmc_hub/app/jobs/reconcile_ordre_drafts.py

120 lines
4.2 KiB
Python

"""
Reconcile ordre draft sync lifecycle.
Promotes sync_status based on known economic references on ordre_drafts:
- pending/failed + economic_order_number -> exported
- exported + economic_invoice_number -> posted
"""
import logging
from typing import Any, Dict, List
from app.core.database import execute_query, get_db_connection, release_db_connection
from app.services.economic_service import get_economic_service
from psycopg2.extras import RealDictCursor
logger = logging.getLogger(__name__)
async def reconcile_ordre_drafts_sync_status(apply_changes: bool = True) -> Dict[str, Any]:
"""Reconcile ordre_drafts sync statuses and optionally persist changes."""
drafts = execute_query(
"""
SELECT id, sync_status, economic_order_number, economic_invoice_number
FROM ordre_drafts
ORDER BY id ASC
""",
(),
) or []
changes: List[Dict[str, Any]] = []
invoice_status_cache: Dict[str, str] = {}
economic_service = get_economic_service()
for draft in drafts:
current = (draft.get("sync_status") or "pending").strip().lower()
target = current
if current in {"pending", "failed"} and draft.get("economic_order_number"):
target = "exported"
if target == "exported" and draft.get("economic_invoice_number"):
target = "posted"
invoice_number = str(draft.get("economic_invoice_number") or "").strip()
if invoice_number:
if invoice_number not in invoice_status_cache:
invoice_status_cache[invoice_number] = await economic_service.get_invoice_lifecycle_status(invoice_number)
lifecycle = invoice_status_cache[invoice_number]
if lifecycle == "paid":
target = "paid"
elif lifecycle in {"booked", "unpaid"} and target in {"pending", "failed", "exported"}:
target = "posted"
if target != current:
changes.append(
{
"draft_id": draft.get("id"),
"from": current,
"to": target,
"economic_invoice_number": invoice_number or None,
}
)
if apply_changes and changes:
conn = get_db_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
for change in changes:
cursor.execute(
"""
UPDATE ordre_drafts
SET sync_status = %s,
last_sync_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
last_exported_at = CASE
WHEN %s IN ('exported', 'posted', 'paid') THEN CURRENT_TIMESTAMP
ELSE last_exported_at
END
WHERE id = %s
""",
(change["to"], change["to"], change["draft_id"]),
)
cursor.execute(
"""
INSERT INTO ordre_draft_sync_events (
draft_id,
event_type,
from_status,
to_status,
event_payload,
created_by_user_id
) VALUES (%s, %s, %s, %s, %s::jsonb, NULL)
""",
(
change["draft_id"],
"sync_status_reconcile",
change["from"],
change["to"],
'{"source":"reconcile_job"}',
),
)
conn.commit()
except Exception:
conn.rollback()
raise
finally:
release_db_connection(conn)
logger.info(
"✅ Reconciled ordre draft sync status: %s changes (%s)",
len(changes),
"applied" if apply_changes else "preview",
)
return {
"status": "applied" if apply_changes else "preview",
"change_count": len(changes),
"changes": changes,
}