diff --git a/backend/tests/test_api_semantics_and_utc.py b/backend/tests/test_api_semantics_and_utc.py index 26de2f1..6c8d14a 100644 --- a/backend/tests/test_api_semantics_and_utc.py +++ b/backend/tests/test_api_semantics_and_utc.py @@ -112,3 +112,39 @@ def test_bootstrap_schema_contains_migrated_core_columns_and_tables(): for snippet in required_snippets: assert snippet in schema_sql + + +def test_insert_engine_event_serializes_nested_datetimes(): + from indian_paper_trading_strategy.engine.db import insert_engine_event + + class FakeCursor: + def __init__(self): + self.params = None + + def execute(self, _sql, params): + self.params = params + + cursor = FakeCursor() + event_ts = datetime(2026, 4, 15, 9, 0, tzinfo=timezone.utc) + payload_ts = datetime(2026, 4, 15, 9, 1, tzinfo=timezone.utc) + + insert_engine_event( + cursor, + "ORDER_PLACED", + data={ + "order_id": "order-1", + "timestamp": payload_ts, + "history": [payload_ts], + }, + meta={"checked_at": payload_ts}, + ts=event_ts, + user_id="user-1", + run_id="run-1", + ) + + data_json = cursor.params[4] + meta_json = cursor.params[6] + + assert data_json.adapted["timestamp"] == payload_ts.isoformat() + assert data_json.adapted["history"] == [payload_ts.isoformat()] + assert meta_json.adapted["checked_at"] == payload_ts.isoformat() diff --git a/indian_paper_trading_strategy/engine/db.py b/indian_paper_trading_strategy/engine/db.py index 4b913d8..20e7d73 100644 --- a/indian_paper_trading_strategy/engine/db.py +++ b/indian_paper_trading_strategy/engine/db.py @@ -2,8 +2,10 @@ import os import threading import time from contextlib import contextmanager -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone from contextvars import ContextVar +from decimal import Decimal +from uuid import UUID import psycopg2 from psycopg2 import pool @@ -142,8 +144,24 @@ def db_transaction(): raise -def _utc_now(): - return datetime.utcnow().replace(tzinfo=timezone.utc) +def _utc_now(): + return datetime.utcnow().replace(tzinfo=timezone.utc) + + +def _json_safe(value): + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, date): + return value.isoformat() + if isinstance(value, Decimal): + return float(value) + if isinstance(value, UUID): + return str(value) + if isinstance(value, dict): + return {str(key): _json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple, set)): + return [_json_safe(item) for item in value] + return value def set_context(user_id: str | None, run_id: str | None): @@ -323,14 +341,14 @@ def insert_engine_event( INSERT INTO engine_event (user_id, run_id, ts, event, data, message, meta) VALUES (%s, %s, %s, %s, %s, %s, %s) """, - ( - scope_user, - scope_run, - when, - event, - Json(data) if data is not None else None, - message, - Json(meta) if meta is not None else None, + ( + scope_user, + scope_run, + when, + event, + Json(_json_safe(data)) if data is not None else None, + message, + Json(_json_safe(meta)) if meta is not None else None, ), )