amarorn / tests /test_user_wallet_phase4.py
beAnalytic's picture
feat: sync main with feature/superbet-live-inplay
5c04262 verified
Raw
History Blame Contribute Delete
7.89 kB
"""Testes da Fase 4 — Feedback Loop (CSV upload + reconciliação + retreino)."""
from __future__ import annotations
import uuid
from datetime import datetime
import pandas as pd
from ingest.user_transactions.parser import parse_user_csv
from ingest.user_transactions.store import (
load_transactions,
save_transactions_bronze,
)
from pipelines.user_bet_analytics import (
compute_model_errors_heatmap,
compute_wallet_summary,
)
from pipelines.user_bet_reconciliation import (
SnapshotRef,
find_snapshot_for_bet,
pair_placed_with_outcome,
)
from schemas.user_transaction import UserTransactionRow
SAMPLE_CSV = """Dados da carteira para teste
DataHoraDaTransação,Transação,Método de Pagamento,Valor,SaldoEmDinheiro,SaldoEmDinheiroAnterior,SaldoBônus,SaldoBônusAnterior,NomeDoJogo
2026-06-09 19:28:12.694,bilhete confirmado,,0.00,370.17,370.17,0.00,0.00,me-INHS-INPLAY-SB_BR
2026-06-09 19:28:10.215,bilhete colocado,,15.00,370.17,385.17,0.00,0.00,me-INHS-INPLAY-SB_BR
2026-06-09 18:32:23.304,bilhete colocado,,25.00,380.92,405.92,0.00,0.00,me-INHS-INPLAY-SB_BR
2026-06-09 19:06:15.317,valor ganhado,,49.25,430.17,380.92,0.00,0.00,me-INHS-INPLAY-SB_BR
2026-06-09 14:54:26.941,depósito,pix,300.00,449.33,149.33,0.00,0.00,UNKNOWN
"""
class TestParser:
def test_parse_basic_csv(self):
rows = parse_user_csv(SAMPLE_CSV, "teste", str(uuid.uuid4()))
assert len(rows) == 5
assert all(isinstance(r, UserTransactionRow) for r in rows)
assert any(r.is_inplay_bet for r in rows)
def test_parse_recognizes_bet_types(self):
rows = parse_user_csv(SAMPLE_CSV, "teste", str(uuid.uuid4()))
types = {r.transaction_type for r in rows}
assert "bilhete colocado" in types
assert "bilhete confirmado" in types
assert "valor ganhado" in types
assert "depósito" in types
def test_parse_amount_decimals(self):
rows = parse_user_csv(SAMPLE_CSV, "teste", str(uuid.uuid4()))
wins = [r for r in rows if r.is_win]
assert len(wins) == 1
assert abs(wins[0].amount - 49.25) < 1e-3
def test_parse_inplay_flag(self):
rows = parse_user_csv(SAMPLE_CSV, "teste", str(uuid.uuid4()))
inplay_rows = [r for r in rows if r.is_inplay_bet]
non_inplay = [r for r in rows if not r.is_inplay_bet]
assert len(inplay_rows) == 4 # 4 INPLAY-SB_BR
assert len(non_inplay) == 1 # 1 depósito UNKNOWN
def test_parse_empty_returns_empty(self):
rows = parse_user_csv("", "teste", str(uuid.uuid4()))
assert rows == []
class TestStorage:
def test_save_and_load_roundtrip(self, tmp_path, monkeypatch):
from config import settings as s
monkeypatch.setattr(s, "lake_root", tmp_path)
rows = parse_user_csv(SAMPLE_CSV, "teste", "uid1")
save_transactions_bronze(rows, "teste", "uid1")
df = load_transactions("teste")
assert len(df) == 5
assert "transaction_at" in df.columns
assert df["user_id"].iloc[0] == "teste"
class TestPairPlacedWithOutcome:
def test_pair_basic(self):
df = pd.DataFrame([
{
"transaction_at": datetime(2026, 6, 9, 18, 32, 23),
"transaction_type": "bilhete colocado",
"amount": 25.0,
"game_name": "me-INHS-INPLAY-SB_BR",
"user_id": "u",
"upload_id": "uid",
},
{
"transaction_at": datetime(2026, 6, 9, 19, 6, 15),
"transaction_type": "valor ganhado",
"amount": 49.25,
"game_name": "me-INHS-INPLAY-SB_BR",
"user_id": "u",
"upload_id": "uid",
},
])
pairs = pair_placed_with_outcome(df)
assert len(pairs) == 1
assert pairs[0]["stake"] == 25.0
assert pairs[0]["won_amount"] == 49.25
assert pairs[0]["won"] is True
def test_pair_skips_cancelled(self):
df = pd.DataFrame([
{
"transaction_at": datetime(2026, 6, 9, 18, 32, 23),
"transaction_type": "bilhete colocado",
"amount": 25.0,
"game_name": "me-INHS-INPLAY-SB_BR",
"user_id": "u",
"upload_id": "uid",
},
{
"transaction_at": datetime(2026, 6, 9, 18, 32, 25),
"transaction_type": "bilhete cancelado",
"amount": 25.0,
"game_name": "me-INHS-INPLAY-SB_BR",
"user_id": "u",
"upload_id": "uid",
},
])
pairs = pair_placed_with_outcome(df)
assert len(pairs) == 0
def test_pair_only_inplay(self):
df = pd.DataFrame([
{
"transaction_at": datetime(2026, 6, 9, 12, 0, 0),
"transaction_type": "bilhete colocado",
"amount": 5.0,
"game_name": "Slot Game", # casino, não inplay
"user_id": "u",
"upload_id": "uid",
},
])
pairs = pair_placed_with_outcome(df)
assert len(pairs) == 0
class TestFindSnapshotForBet:
def test_match_within_window(self):
snap = SnapshotRef(
event_id=123,
path=__import__("pathlib").Path("/tmp/x.json"),
timestamp=datetime(2026, 6, 9, 22, 30, 0), # UTC
home_team="Brasil",
away_team="Argentina",
is_live=True,
minute=60,
home_score=1,
away_score=0,
)
# Bet em horário local BR (UTC-3) → 19:30 local = 22:30 UTC
bet_at = datetime(2026, 6, 9, 19, 30, 0)
result = find_snapshot_for_bet(bet_at, [snap])
assert result.matched
assert result.snapshot is snap
assert result.confidence > 0.9
def test_no_match_outside_window(self):
snap = SnapshotRef(
event_id=123,
path=__import__("pathlib").Path("/tmp/x.json"),
timestamp=datetime(2026, 6, 9, 22, 30, 0),
home_team="Brasil",
away_team="Argentina",
is_live=True,
)
bet_at = datetime(2026, 6, 9, 19, 0, 0) # 30 min antes (UTC: 22:00 vs 22:30)
result = find_snapshot_for_bet(bet_at, [snap], window_seconds=180)
assert not result.matched
def test_skip_non_live_snapshots(self):
snap = SnapshotRef(
event_id=123,
path=__import__("pathlib").Path("/tmp/x.json"),
timestamp=datetime(2026, 6, 9, 22, 30, 0),
home_team="Brasil",
away_team="Argentina",
is_live=False, # pré-jogo
)
bet_at = datetime(2026, 6, 9, 19, 30, 0)
result = find_snapshot_for_bet(bet_at, [snap])
assert not result.matched
class TestAnalytics:
def test_summary_empty_user(self):
summary = compute_wallet_summary("usuario_inexistente_xyz")
assert summary["n_transactions"] == 0
assert summary["pnl"] == 0.0
def test_summary_with_data(self, tmp_path, monkeypatch):
from config import settings as s
monkeypatch.setattr(s, "lake_root", tmp_path)
rows = parse_user_csv(SAMPLE_CSV, "teste_analytics", "uid_an")
save_transactions_bronze(rows, "teste_analytics", "uid_an")
summary = compute_wallet_summary("teste_analytics")
assert summary["n_transactions"] == 5
assert summary["n_bets_placed"] == 2
assert summary["total_deposits"] == 300.0
# P&L = won (49.25) - net_staked (15+25 = 40 - 0 cancelados) = 9.25
assert abs(summary["pnl"] - 9.25) < 1e-2
def test_model_errors_empty(self):
result = compute_model_errors_heatmap("usuario_xyz_inexistente")
assert result["buckets"] == []