fix(partner-mismatch): fix 3 infinite-loop bugs in mismatch detection cycle

Three root causes caused partner_mismatch=1 to loop indefinitely:

1. No-CUI company orders (is_pj=1, no cod_fiscal): old code flagged as
   mismatch every cycle. Fixed by requiring new_cf to be non-null for
   PF→PJ detection. Stale flags from old code cleared via new
   clear_stale_partner_mismatches_no_cui() for out-of-window orders.

2. same_partner resync path did not update cod_fiscal_gomag in SQLite.
   On next cycle GoMag returned a CUI but stored_cf was still NULL →
   re-detected as mismatch. Fixed by also calling update_partner_resync_data
   (not just update_partner_mismatch_batch) in the same_partner branch.

3. GoMag sends CUI with space: 'RO 17922480'. The _strip_ro() regex
   ^RO left the space → ' 17922480' != '17922480' → false mismatch.
   Fixed: changed regex to ^RO\s* and added .strip().

Also adds diagnostic logger.info lines for mismatch detection/resync counts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-08 17:29:44 +00:00
parent aa581e5cd9
commit 5584dd3c4f
2 changed files with 54 additions and 6 deletions

View File

@@ -1440,6 +1440,36 @@ async def update_partner_mismatch_batch(updates: list) -> None:
await db.close() await db.close()
async def clear_stale_partner_mismatches_no_cui(exclude_numbers: set) -> int:
"""Clear partner_mismatch=1 for orders with cod_fiscal_gomag=NULL that are NOT in the
current sync batch. These were flagged by old code (before the no-CUI fix) and will
never self-correct because they fall outside the active sync window.
Returns number of rows cleared.
"""
db = await get_sqlite()
try:
if exclude_numbers:
placeholders = ",".join("?" * len(exclude_numbers))
sql = f"""
UPDATE orders SET partner_mismatch = 0, updated_at = datetime('now')
WHERE partner_mismatch = 1
AND cod_fiscal_gomag IS NULL
AND order_number NOT IN ({placeholders})
"""
await db.execute(sql, list(exclude_numbers))
else:
await db.execute("""
UPDATE orders SET partner_mismatch = 0, updated_at = datetime('now')
WHERE partner_mismatch = 1 AND cod_fiscal_gomag IS NULL
""")
await db.commit()
cursor = await db.execute("SELECT changes()")
row = await cursor.fetchone()
return row[0] if row else 0
finally:
await db.close()
async def update_partner_resync_data(order_number: str, data: dict) -> None: async def update_partner_resync_data(order_number: str, data: dict) -> None:
"""Update partner fields + clear partner_mismatch after a successful resync.""" """Update partner fields + clear partner_mismatch after a successful resync."""
db = await get_sqlite() db = await get_sqlite()

View File

@@ -641,11 +641,12 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
def _strip_ro(cf): def _strip_ro(cf):
if not cf: if not cf:
return "" return ""
return re.sub(r'^RO', '', cf.strip().upper()) # Strip optional "RO" prefix + any surrounding whitespace
return re.sub(r'^RO\s*', '', cf.strip().upper()).strip()
is_mismatch = False is_mismatch = False
if new_data["is_pj"] and not stored_cf: if new_data["is_pj"] and new_cf and not stored_cf:
is_mismatch = True # PF→PJ is_mismatch = True # PF→PJ (doar dacă are CUI — fără CUI nu putem confirma)
elif not new_data["is_pj"] and stored_cf: elif not new_data["is_pj"] and stored_cf:
is_mismatch = True # PJ→PF is_mismatch = True # PJ→PF
elif new_data["is_pj"] and stored_cf and _strip_ro(new_cf) != _strip_ro(stored_cf): elif new_data["is_pj"] and stored_cf and _strip_ro(new_cf) != _strip_ro(stored_cf):
@@ -657,17 +658,28 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
await sqlite_service.update_partner_mismatch_batch(mismatch_updates) await sqlite_service.update_partner_mismatch_batch(mismatch_updates)
# Clear stale mismatches for orders outside the current sync window
# that have no CUI stored (flagged by old code before the no-CUI fix)
current_batch_numbers = {o.number for o in already_in_roa}
cleared = await sqlite_service.clear_stale_partner_mismatches_no_cui(current_batch_numbers)
if cleared:
logger.info(f"Partner mismatch: cleared {cleared} stale no-CUI flags from previous sync window")
# Auto-resync uninvoiced orders with partner mismatch (max 5/cycle) # Auto-resync uninvoiced orders with partner mismatch (max 5/cycle)
MAX_PARTNER_RESYNC_PER_CYCLE = 5 MAX_PARTNER_RESYNC_PER_CYCLE = 5
total_mismatched = sum(1 for v in mismatch_map.values() if v == 1)
logger.info(f"Partner mismatch detection: {len(already_in_roa)} orders checked, {total_mismatched} mismatches found")
mismatched_uninvoiced = [ mismatched_uninvoiced = [
o for o in already_in_roa o for o in already_in_roa
if mismatch_map.get(o.number) == 1 if mismatch_map.get(o.number) == 1
and not stored_partner_data.get(o.number, {}).get("factura_numar") and not stored_partner_data.get(o.number, {}).get("factura_numar")
][:MAX_PARTNER_RESYNC_PER_CYCLE] ][:MAX_PARTNER_RESYNC_PER_CYCLE]
logger.info(f"Partner auto-resync: {len(mismatched_uninvoiced)} uninvoiced orders queued")
if mismatched_uninvoiced: if mismatched_uninvoiced:
resync_ok = 0 resync_ok = 0
for _order in mismatched_uninvoiced: for _order in mismatched_uninvoiced:
logger.info(f"Partner resync attempt: #{_order.number}")
try: try:
await _resync_partner_for_order( await _resync_partner_for_order(
order=_order, order=_order,
@@ -676,6 +688,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
run_id=run_id, run_id=run_id,
) )
resync_ok += 1 resync_ok += 1
logger.info(f"Partner resync success: #{_order.number}")
except Exception as _e: except Exception as _e:
_log_line(run_id, f"#{_order.number} EROARE resync partener: {_e}") _log_line(run_id, f"#{_order.number} EROARE resync partener: {_e}")
logger.error(f"Partner resync error for {_order.number}: {_e}") logger.error(f"Partner resync error for {_order.number}: {_e}")
@@ -1310,9 +1323,14 @@ async def _resync_partner_for_order(order, stored: dict, app_settings: dict, run
resync_result = await asyncio.to_thread(_do_resync) resync_result = await asyncio.to_thread(_do_resync)
if resync_result.get("same_partner"): if resync_result.get("same_partner"):
await sqlite_service.update_partner_mismatch_batch([ # Update cod_fiscal_gomag so next detection doesn't re-flag this order
{"order_number": order_number, "partner_mismatch": 0} await sqlite_service.update_partner_resync_data(order_number, {
]) "id_partener": resync_result["new_partner_id"],
"cod_fiscal_gomag": cod_fiscal_override or new_partner_data["cod_fiscal"],
"cod_fiscal_roa": None,
"denumire_roa": stored.get("denumire_roa"),
"partner_mismatch": 0,
})
_log_line(run_id, f"#{order_number} RESYNC: partener neschimbat, mismatch cleared") _log_line(run_id, f"#{order_number} RESYNC: partener neschimbat, mismatch cleared")
else: else:
new_partner_id = resync_result["new_partner_id"] new_partner_id = resync_result["new_partner_id"]