"""G1 -- a transaction traverses all seven services and returns a coherent result.""" import time import uuid import psycopg import pytest from harness.payloads import tx_payload pytestmark = pytest.mark.contract async def test_valid_transaction_returns_full_envelope(api, manifest, fresh_balances): """G1: a valid transaction returns 201 with the full envelope and coherent fields.""" account_id = manifest["clean_account_id"] fresh_balances(account_id) resp = await api.post("/api/transactions", json=tx_payload(account_id)) assert resp.status_code == 201, resp.text body = resp.json() uuid.UUID(body["transaction_id"]) assert body["status"] in ("settled", "rejected") assert 0.0 <= body["risk_score"] <= 1.0 assert isinstance(body["balance_minor"], int) assert "summary" in body uuid.UUID(body["request_id"]) assert body["elapsed_ms"] > 0 async def test_transaction_is_durable(api, manifest, fresh_balances, dsn): """G1: the transaction row exists with a terminal status within 2s.""" account_id = manifest["clean_account_id"] fresh_balances(account_id) resp = await api.post("/api/transactions", json=tx_payload(account_id)) assert resp.status_code == 201 tx_id = resp.json()["transaction_id"] deadline = time.monotonic() + 2 status = None with psycopg.connect(dsn, autocommit=True) as conn: while time.monotonic() < deadline: with conn.cursor() as cur: cur.execute("SELECT status FROM transactions WHERE id = %s", (tx_id,)) row = cur.fetchone() if row and row[0] in ("settled", "rejected"): status = row[0] break assert status in ("settled", "rejected"), f"transaction {tx_id} never reached a terminal status" async def test_deposit_increases_balance_by_exact_amount(api, manifest, fresh_balances, dsn): """G1: a settled deposit moves the balance up by exactly amount_minor.""" account_id = manifest["clean_account_id"] fresh_balances(account_id) with psycopg.connect(dsn, autocommit=True) as conn: with conn.cursor() as cur: cur.execute("SELECT balance_minor FROM accounts WHERE id = %s", (account_id,)) (before,) = cur.fetchone() amount = 5000 resp = await api.post( "/api/transactions", json=tx_payload(account_id, kind="deposit", amount_minor=amount) ) assert resp.status_code == 201 body = resp.json() if body["status"] == "settled": assert body["balance_minor"] == before + amount async def test_withdrawal_decreases_balance_by_exact_amount(api, manifest, fresh_balances, dsn): """G1: a settled withdrawal moves the balance down by exactly amount_minor.""" account_id = manifest["clean_account_id"] fresh_balances(account_id) with psycopg.connect(dsn, autocommit=True) as conn: with conn.cursor() as cur: cur.execute("SELECT balance_minor FROM accounts WHERE id = %s", (account_id,)) (before,) = cur.fetchone() amount = 1000 resp = await api.post( "/api/transactions", json=tx_payload(account_id, kind="withdrawal", amount_minor=amount) ) assert resp.status_code == 201 body = resp.json() if body["status"] == "settled": assert body["balance_minor"] == before - amount async def test_sanctioned_holder_scores_higher_risk_than_clean(api, manifest, fresh_balances): """G1: a sanctioned account yields a materially higher risk_score than a clean one (assert the ordering, not a value).""" clean_id = manifest["clean_account_id"] sanctioned_id = manifest["sanctioned_account_ids"][0] fresh_balances(clean_id) fresh_balances(sanctioned_id) clean_resp = await api.post("/api/transactions", json=tx_payload(clean_id)) sanctioned_resp = await api.post("/api/transactions", json=tx_payload(sanctioned_id)) assert clean_resp.status_code == 201 assert sanctioned_resp.status_code == 201 clean_score = clean_resp.json()["risk_score"] sanctioned_score = sanctioned_resp.json()["risk_score"] assert sanctioned_score > clean_score, ( f"expected sanctioned account risk_score ({sanctioned_score}) > clean ({clean_score})" ) async def test_insights_returns_summary_for_seeded_account(api, manifest): """G1: GET /api/accounts/{id}/insights returns a summary for a seeded account.""" resp = await api.get(f"/api/accounts/{manifest['clean_account_id']}/insights") assert resp.status_code == 200 assert isinstance(resp.json(), dict) and resp.json() async def test_unknown_account_returns_404(api): """G1: submitting a transaction for an unknown account returns 404 with the error envelope.""" resp = await api.post("/api/transactions", json=tx_payload(str(uuid.uuid4()))) assert resp.status_code == 404 body = resp.json() assert "error" in body and "request_id" in body async def test_negative_amount_on_deposit_is_rejected(api, manifest): """G1: a negative amount_minor on a deposit returns 400 or 422.""" resp = await api.post( "/api/transactions", json=tx_payload(manifest["clean_account_id"], kind="deposit", amount_minor=-500), ) assert resp.status_code in (400, 422), resp.text async def test_missing_required_field_returns_422(api, manifest): """G1: a missing required field returns 422.""" payload = tx_payload(manifest["clean_account_id"]) del payload["kind"] resp = await api.post("/api/transactions", json=payload) assert resp.status_code == 422