feat(alerts): publish reconciliation quantity mismatches to trex-notifier (req-0007/ac-0024)

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-25 14:50:29 +00:00
committed by GitHub
co-authored by genichin
parent baae02d2af
commit 153a563f6d
6 changed files with 1458 additions and 52 deletions
@@ -0,0 +1,122 @@
"""PostgreSQL-backed durable reconcile notification lifecycle store (req-0007, ac-0024).
Tracks per-identity B/C-case mismatch notification fingerprints so that
duplicate alarms are suppressed across scheduler ticks and process restarts,
and pending resolved transitions can be retried.
PK: identity_key (slot_id for B-case; "{instance_key}\\x00C\\x00{symbol}" for C-case).
lifecycle_state: 'notified' | 'pending_resolved'.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from trex_core.adapters.persistence.postgresql import _require_psycopg
class PostgreSQLReconcileNotificationStore:
"""`reconcile_notification_state` table CRUD (req-0007, ac-0024)."""
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
self._psycopg = _require_psycopg(psycopg_module)
self._dsn = dsn
def get_state(self, identity_key: str) -> dict | None:
"""Return state dict or None."""
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
cur.execute(
"SELECT identity_key, case_code, instance_key, symbol, "
" quantity_a, quantity_b, lifecycle_state "
"FROM reconcile_notification_state "
"WHERE identity_key = %s LIMIT 1",
(identity_key,),
)
row = cur.fetchone()
if row is None:
return None
return {
"identity_key": row[0],
"case_code": row[1],
"instance_key": row[2],
"symbol": row[3],
"quantity_a": row[4],
"quantity_b": row[5],
"lifecycle_state": row[6],
}
def set_state(
self,
identity_key: str,
*,
case_code: str,
instance_key: str,
symbol: str,
quantity_a: int,
quantity_b: int,
lifecycle_state: str,
) -> None:
"""Upsert notification state."""
now = datetime.now(tz=UTC)
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO reconcile_notification_state "
"(identity_key, case_code, instance_key, symbol, "
" quantity_a, quantity_b, lifecycle_state, created_at, updated_at) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) "
"ON CONFLICT (identity_key) DO UPDATE "
"SET case_code = EXCLUDED.case_code, "
" instance_key = EXCLUDED.instance_key, "
" symbol = EXCLUDED.symbol, "
" quantity_a = EXCLUDED.quantity_a, "
" quantity_b = EXCLUDED.quantity_b, "
" lifecycle_state = EXCLUDED.lifecycle_state, "
" updated_at = EXCLUDED.updated_at",
(
identity_key,
case_code,
instance_key,
symbol,
quantity_a,
quantity_b,
lifecycle_state,
now,
now,
),
)
conn.commit()
def clear_state(self, identity_key: str) -> None:
"""Delete state row (called after successful resolved publish)."""
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM reconcile_notification_state WHERE identity_key = %s",
(identity_key,),
)
conn.commit()
def list_states_by_lifecycle(self, lifecycle_state: str) -> list[dict]:
"""Return all state dicts with the given lifecycle_state."""
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
cur.execute(
"SELECT identity_key, case_code, instance_key, symbol, "
" quantity_a, quantity_b, lifecycle_state "
"FROM reconcile_notification_state "
"WHERE lifecycle_state = %s",
(lifecycle_state,),
)
rows = cur.fetchall()
return [
{
"identity_key": row[0],
"case_code": row[1],
"instance_key": row[2],
"symbol": row[3],
"quantity_a": row[4],
"quantity_b": row[5],
"lifecycle_state": row[6],
}
for row in rows
]