diff --git a/SOP.md b/SOP.md new file mode 100644 index 0000000..d051b5a --- /dev/null +++ b/SOP.md @@ -0,0 +1,481 @@ +# SOP.md — Prototipo Gioco Carte (Server Docker + Client Test) + +> Obiettivo: realizzare un prototipo end-to-end dove: +> - Utente si registra/login con email+password +> - Ha una valuta (Gold) con cui apre bauli +> - Aprendo un baule ottiene carte con rarità +> - La collezione è "unique-only": la carta viene salvata solo se non già posseduta +> - Se la carta è già posseduta: conversione in Gold (o altra regola definita) +> - I giocatori possono vedere la collezione pubblica di altri giocatori +> - Server e DB girano su Docker +> - Esiste un client temporaneo testabile (CLI o Postman) per test E2E + +--- + +## 0) Decisioni bloccanti (DA DEFINIRE PRIMA DI CODIFICARE) +Compilare questa sezione PRIMA di procedere. + +- [ ] STACK_BACKEND: TODO (FastAPI / NestJS) +- [ ] DB: PostgreSQL (default) +- [ ] ORM: TODO (SQLAlchemy+Alembic / Prisma / TypeORM) +- [ ] AUTH: TODO (JWT access + refresh / solo access) +- [ ] CLIENT_TEST: TODO (CLI Python / Postman / entrambi) +- [ ] CHEST: + - gold_initial: 1000 + - chest_cost: 100 + - cards_per_open: 3 + - rarities: Common 70%, Rare 25%, Legendary 5% + - duplicate_conversion_gold: 20 +- [ ] PRIVACY: + - collection_public_default: true + +> Regola: se un valore è “TODO” non procedere oltre senza definirlo. + +--- + +## 1) Regole Architetturali (NON negoziabili) +- Tutta la logica RNG e assegnazione premi sta nel backend (mai nel client). +- Il backend è l’unica sorgente di verità. Il client visualizza e chiama API. +- L’apertura baule deve essere: + - atomica (transazione DB) + - idempotente (retry rete non deve duplicare premi né spesa) +- La collezione non deve avere duplicati: + - vincolo DB: PRIMARY KEY (user_id, card_id) + +--- + +## 2) Convenzioni Repo e Commit +### 2.1 Struttura repository +Creare repo con questa struttura: + +card-game/ +- backend/ +- client-test/ (CLI o Postman) +- infra/ +- spec/ +- docker-compose.yml +- .env.example +- README.md +- SOP.md + +### 2.2 Regole commit +- Ogni step sotto corrisponde a 1 commit (o più commit piccoli) con un messaggio chiaro: + - feat: ... + - test: ... + - chore: ... +- Ogni commit deve avere un “Gate” (test di verifica) che deve passare. + +--- + +## 3) Stack locale con Docker (Backend + DB) +### 3.1 File obbligatori +- docker-compose.yml +- backend/Dockerfile +- backend/.dockerignore +- .env.example + +### 3.2 Variabili ambiente minime (.env) +Definire: +- APP_ENV=local +- DB_HOST=db +- DB_PORT=5432 +- DB_NAME=cardgame +- DB_USER=cardgame +- DB_PASSWORD=cardgame +- JWT_SECRET=change-me +- JWT_ACCESS_TTL_MIN=15 +- JWT_REFRESH_TTL_DAYS=30 + +> Non committare mai `.env` vero. Solo `.env.example`. + +--- + +# STEPS (commit-based) + +## STEP 01 — Bootstrap repo + Docker “Hello World” +### Obiettivo +- `docker compose up --build` avvia tutto +- backend espone `GET /health` → `{ "status": "ok" }` + +### Azioni +1) Creare docker-compose.yml con: + - service `db` (postgres) + - service `api` (backend) +2) Implementare endpoint `/health` +3) Documentare README con comandi minimi + +### Output +- docker-compose.yml +- backend/Dockerfile +- backend/app/main.(py|ts) +- README.md + +### Commit +- `feat: bootstrap docker compose and health endpoint` + +### Gate / Test +- Comando: `docker compose up --build` +- Verifica: + - `curl http://localhost:8000/health` + - Risposta deve essere `{"status":"ok"}` + +--- + +## STEP 02 — Connessione DB + endpoint db-check +### Obiettivo +Backend connesso realmente a Postgres. + +### Azioni +1) Aggiungere driver DB e configurazione. +2) Implementare `GET /db-check` che esegue `SELECT 1`. + +### Commit +- `feat: add database connection and db-check endpoint` + +### Gate / Test +- `curl http://localhost:8000/db-check` +- Risposta: `{ "db": "ok" }` + +--- + +## STEP 03 — Schema DB v1 (tabelle core) +### Obiettivo +Creare schema DB minimo completo per il prototipo. + +### Tabelle richieste (v1) +#### users +- id (uuid / serial) +- email (unique, not null) +- password_hash +- created_at + +#### user_profiles +- user_id (PK/FK users.id) +- nickname (unique, not null) +- is_collection_public (bool, default true) +- created_at + +#### wallets +- user_id (PK/FK) +- balance (int, not null) + +#### wallet_transactions +- id +- user_id +- type (CREDIT/DEBIT) +- amount +- reason (REGISTER_BONUS, CHEST_OPEN, DUPLICATE_CONVERT, etc.) +- reference_id (nullable, e.g. chest_open_id) +- created_at + +#### cards (catalogo) +- id (string or uuid) +- name +- rarity (COMMON/RARE/LEGENDARY) +- created_at + +#### chests (catalogo) +- id +- name +- cost_gold +- cards_per_open +- created_at + +#### user_cards (collezione unique-only) +- user_id +- card_id +- obtained_at +- PRIMARY KEY (user_id, card_id) + +#### chest_opens (audit) +- id +- user_id +- chest_id +- spent_gold +- created_at + +#### chest_open_items (audit dettagli) +- id +- chest_open_id +- card_id (nullable se conversion-only) +- rarity +- outcome_type (NEW / CONVERTED) +- converted_gold (nullable) +- created_at + +#### idempotency_keys +- key (string) +- user_id +- endpoint (e.g. "POST:/chests/{id}/open") +- response_body (json/text) +- created_at +- UNIQUE (key, user_id, endpoint) + +### Azioni +- Implementare migrazioni (consigliato) o create_all (solo prototipo). +- Assicurare vincoli unici e FK. + +### Commit +- `feat: add database schema v1` + +### Gate / Test +- Avviare stack e verificare che tabelle esistano (psql o query). +- Verificare vincolo UNIQUE email e nickname. +- Verificare PK composita su user_cards. + +--- + +## STEP 04 — Seed catalogo (cards + chests) +### Obiettivo +Popolare catalogo con dati minimi. + +### Azioni +- Inserire almeno: + - 10 cards (mix rarità) + - 1 chest base (cost=100, cards_per_open=3) +- Seed deve essere ripetibile (non duplicare se rilanciato). + +### Commit +- `feat: seed initial catalog (cards, chests)` + +### Gate / Test +- `GET /catalog/cards` ritorna lista > 0 +- `GET /catalog/chests` ritorna lista > 0 + +--- + +## STEP 05 — Auth: register + login (email/password) +### Obiettivo +Utente può registrarsi e loggarsi. + +### Azioni +Implementare: +- `POST /auth/register`: + - valida email e password (minimo) + - hash password + - crea user + - crea profile con nickname (TODO: decidere quando si setta nickname) + - crea wallet con gold_initial + - crea wallet_transaction REGISTER_BONUS +- `POST /auth/login`: + - verifica credenziali + - emette token (access + refresh se previsto) + +> Nota: nickname +- Opzione A: richiesto in register (consigliato per social immediato) +- Opzione B: scelto dopo login (richiede endpoint `/me/profile` update) + +### Commit +- `feat: implement register and login` + +### Gate / Test (manuale) +- Register nuovo: 201 +- Register email già usata: 409 +- Login corretto: 200 + token +- Login password errata: 401 + +--- + +## STEP 06 — Endpoint me: wallet + collection + profile +### Obiettivo +Utente autenticato può vedere: +- saldo +- collezione +- profilo + +### Azioni +Implementare: +- `GET /me/wallet` +- `GET /me/collection` +- `GET /me/profile` + +### Commit +- `feat: add me endpoints (wallet, collection, profile)` + +### Gate / Test +- Dopo register/login: + - wallet = gold_initial + - collection = [] + - profile contiene nickname + +--- + +## STEP 07 — Core: Open Chest (transazione + unique-only + conversione) +### Obiettivo +Endpoint che apre il baule con logica corretta e audit completo. + +### Endpoint +- `POST /chests/{chestId}/open` +Header: +- Authorization: Bearer +- Idempotency-Key: + +### Algoritmo (obbligatorio) +Dentro una transazione DB: +1) Validare chestId +2) Verificare saldo >= cost +3) Debit saldo (wallet) +4) Per i=1..cards_per_open: + 4.1) Roll rarità secondo pesi + 4.2) Selezionare una carta casuale tra quelle di quella rarità NON possedute dall’utente + - Se pool vuoto: + - outcome = CONVERTED (converted_gold = duplicate_conversion_gold) + - credit wallet + - wallet_transaction DUPLICATE_CONVERT + - Se pool non vuoto: + - inserire in user_cards (PK impedisce dup) + - outcome = NEW +5) Salvare chest_opens + chest_open_items +6) Commit + +### Idempotenza +- Se la stessa Idempotency-Key per lo stesso user+endpoint è già stata usata: + - ritornare ESATTAMENTE la response salvata + - NON scalare saldo + - NON creare nuovi record + +### Commit +- `feat: implement open chest with transaction, unique-only, conversion, audit` + +### Gate / Test (manuale) +Caso A (happy path): +- wallet iniziale 1000 +- open chest +- wallet diminuisce di 100 (+ eventuali conversioni) +- collection cresce con nuove carte +- chest_opens creato +- chest_open_items creati = cards_per_open + +Caso B (saldo insufficiente): +- set wallet a 50 (o creare user con meno gold) +- open chest → 403 + +Caso C (idempotenza): +- inviare due volte la stessa request con stessa Idempotency-Key +- wallet deve scalare una volta sola +- response identica + +Caso D (no duplicates): +- aprire fino a completare tutte le carte (o tutte della rarità) +- ulteriori aperture producono conversioni, senza violare vincoli DB + +--- + +## STEP 08 — Social: profilo pubblico + collezione altrui +### Obiettivo +Chiunque autenticato può vedere collezioni altrui (se pubbliche). + +### Endpoint +- `GET /users/{nickname}` +- `GET /users/{nickname}/collection` + +### Regole privacy +- se `is_collection_public=false` → 403 (o response “private” definita) + +### Commit +- `feat: add public user profile and collection endpoints` + +### Gate / Test +- Utente A apre carte +- Utente B fa GET su collezione A → vede lista +- Se A setta privacy false (TODO se implementare endpoint) → B riceve 403 + +--- + +## STEP 09 — Test automatici backend (minimo indispensabile) +### Obiettivo +Test ripetibili per le feature core. + +### Test richiesti +- register/login +- wallet iniziale +- open chest happy path +- open chest saldo insufficiente +- open chest idempotenza +- unique-only: non si possono inserire duplicati + +### Commit +- `test: add backend tests for core flows` + +### Gate / Test +- `pytest` (o test runner equivalente) verde + +--- + +## STEP 10 — Client di test temporaneo (CLI o Postman) +### Obiettivo +Avere un modo semplice per fare E2E senza Unity. + +### Opzione 1: CLI (consigliata) +- script che: + - register + - login + - open chest con Idempotency-Key + - stampa wallet e collection + - view other collection + +### Opzione 2: Postman collection +- file esportato con env (base_url, tokens) + +### Commit +- `feat: add test client (cli/postman) for end-to-end runs` + +### Gate / Test +- Eseguire scenario: + 1) crea userA e userB + 2) userA open chest 3 volte + 3) userB legge collezione userA + 4) verificare che non ci siano duplicati in userA + +--- + +## STEP 11 — Hardening minimo (prototipo, ma non fragile) +### Obiettivo +Ridurre bug e abusi evidenti. + +### Azioni +- rate limit su login e open chest +- logging strutturato per chest_open +- gestione errori uniforme (JSON: code, message) + +### Commit +- `chore: add basic rate limiting and structured logs` + +### Gate / Test +- aprire 20 volte in 1 secondo → deve limitare o rispondere coerentemente +- log contiene open_id e user_id + +--- + +# 4) Definition of Done (DoD) Prototipo +Il prototipo è “DONE” quando: +- [ ] docker compose up avvia stack +- [ ] register/login funzionano +- [ ] wallet non scende mai sotto zero +- [ ] open chest è transazionale e idempotente +- [ ] user_cards non contiene duplicati (vincolo DB) +- [ ] collezione pubblica di altri utenti è consultabile +- [ ] esistono test automatici minimi +- [ ] esiste client di test per E2E + +--- + +# 5) Prompt template per agente IA (uso consigliato) +## Prompt generale per ogni step +“In questo step implementa SOLO ciò che è richiesto, senza inventare funzionalità extra. +Rispetta i Gate/Test e aggiorna README se cambiano comandi. +Dopo ogni modifica, elenca i file creati/modificati.” + +## Prompt per open chest (molto importante) +“Implementa `POST /chests/{chestId}/open` ESATTAMENTE secondo SOP. +Vincoli: transazione DB, idempotenza con Idempotency-Key, unique-only, conversione duplicati, audit. +Non cambiare comportamento e non introdurre logiche non esplicitate.” + +--- + +# 6) Domande aperte (non procedere se non risolte) +- STACK_BACKEND = ? +- AUTH (refresh token sì/no) = ? +- CLIENT_TEST = ? +- Conversione duplicati: sempre 20 gold o dipende da rarità? (se dipende, definire tabella) +- Nickname: in register o endpoint separato? (definire) diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..00f0307 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DB_HOST: str + DB_PORT: str = "5432" + DB_USER: str + DB_PASSWORD: str + DB_NAME: str + + class Config: + env_file = ".env" + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..d233846 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,17 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from app.config import settings + +SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" + +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py index 2f309e7..53de31a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,19 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import text +from app.database import get_db app = FastAPI(title="Card Game Backend") @app.get("/health") def health_check(): return {"status": "ok"} + +@app.get("/db-check") +def db_check(db: Session = Depends(get_db)): + try: + # Execute simple query to check connection + db.execute(text("SELECT 1")) + return {"db": "ok"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}") diff --git a/backend/requirements.txt b/backend/requirements.txt index 3c2e4dd..98e4aec 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,4 @@ uvicorn sqlalchemy psycopg2-binary asyncpg +pydantic-settings diff --git a/docker-compose.yml b/docker-compose.yml index c989350..a9865ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,9 @@ services: - "8000:8000" environment: - DB_HOST=postgres + - DB_PORT=5432 - DB_USER=${POSTGRES_USER} - - DB_PASS=${POSTGRES_PASSWORD} + - DB_PASSWORD=${POSTGRES_PASSWORD} - DB_NAME=${POSTGRES_DB} depends_on: - postgres