# Draw 'n' Roll — Backend Handover

This package is everything the backend team needs to build the real server for the Draw 'n' Roll
Unity client, plus a runnable mock of that server so the client can be exercised today.

## What's inside

| Path | What it is |
|---|---|
| `API_SCHEMA.md` | **The contract.** Every endpoint, request/response shape, status code, DB sketch, and the open decisions the backend team owns (§9). Stack-agnostic REST/JSON. |
| `mock-server/` | A zero-dependency Node implementation of the whole contract, in memory. Not a reference architecture — no persistence, no real auth, no receipt verification — but it honours every response shape, so the client works against it end to end. |
| `DrawAndRoll.postman_collection.json` | A contract test suite: 39 requests / 134 assertions covering every endpoint, the conflict paths (409/422/401/400/404) and the invariants the client silently depends on. **Green against the mock. Run it against the real backend as the acceptance test.** |

## Deploying on the live server

The live host already serves another game's Nakama server on the same public IP, and port 5000 is
taken. Two decisions follow from that, both already reflected in this package:

- **Port 5055.** The app listens on `PORT` from the environment, defaulting to **5055** — unprivileged,
  clear of Nakama's 7348–7351 block, and clear of 5000. Confirm it is free before starting:
  `ss -tlnp | grep 5055`.
- **Bind to localhost and front it with a subdomain on 443.** Do not expose 5055 publicly. Route by
  hostname through the existing web server so both games share the public IP cleanly:

  ```
  api.drawandroll.<domain>   →  127.0.0.1:5055    (this API)
  <nakama-host>.<domain>     →  127.0.0.1:7350    (the other game)
  ```

  Only 443 stays public, cPanel's AutoSSL issues the certificate for the subdomain, and the game
  talks HTTPS — which it must: Android 9+ and iOS ATS both block plain HTTP by default, so a raw
  `http://host:5055` would need per-platform exceptions in the Unity build. With the subdomain the
  client's `baseUrl` is simply `https://api.drawandroll.<domain>/api` — no port in it, so it never
  changes if the backend moves ports later.

### Option A — cPanel "Setup Node.js App" (recommended)

This is Passenger-managed and does the proxying for you; you never pick a port at all.

1. In cPanel create the subdomain `api.drawandroll.<domain>` (Domains → Create a New Domain) and let
   AutoSSL issue its certificate.
2. Upload and extract this archive (e.g. to `~/drawandroll-api`).
3. cPanel → **Setup Node.js App** → *Create Application*:
   - Node.js version: **18 or newer**; Application mode: Production
   - Application root: `drawandroll-api/mock-server`
   - Application URL: the `api.drawandroll.<domain>` subdomain, at its root
   - Application startup file: `server.js`
4. Environment variables:
   - `API_PREFIX=/api/v1` — the client calls `<baseUrl>/v1/...` with a `baseUrl` ending in `/api`,
     so the server must answer at `/api/v1/...`. If Passenger strips part of the path before it
     reaches the app, set this to whatever remains (verify with the health check).
   - `WIN_REWARD_COINS` — coins granted for a reported win. Default `100`.
   - `PORT` — leave unset; Passenger supplies it.
5. *Create* → *Run*. Check: `https://api.drawandroll.<domain>/health` →
   `{"success":true,"data":{"status":"up"}}`.

### Option B — run it yourself (PM2 / systemd / Docker) behind Apache

1. Start the app bound to localhost on 5055:
   - PM2: `PORT=5055 pm2 start server.js --name drawandroll-api`
   - Docker: `cd mock-server && docker compose up -d --build` (compose maps `5055:5055`; edit the
     host side of that mapping if 5055 is ever taken)
2. Proxy the subdomain to it. In the subdomain's vhost (cPanel: *Apache Configuration* include, or
   `.htaccess` if `mod_proxy` is enabled for the account):
   ```apache
   ProxyPreserveHost On
   ProxyPass        "/" "http://127.0.0.1:5055/"
   ProxyPassReverse "/" "http://127.0.0.1:5055/"
   ```
3. Keep `API_PREFIX` at its default `/api/v1` — nothing is stripped in this setup.
4. Same health check as above.

## Pointing the Unity client at it

In the Unity project, `Assets/Api/Resources/ApiConfig.asset` → `baseUrl` = `https://api.drawandroll.<domain>/api`
(no trailing slash; the client appends `/v1/...`). For local development it points at
`http://localhost:5055/api`. Leave `appToken` empty for the mock.

## Running the acceptance suite

With [Postman CLI](https://learning.postman.com/docs/postman-cli/postman-cli-overview/) installed:

```bash
postman collection run DrawAndRoll.postman_collection.json --env-var "baseUrl=https://api.drawandroll.<domain>/api/v1"
```

Or import the collection into the Postman app and change the `baseUrl` collection variable.

Against a **real** backend, skip the folders `0. Setup (mock only)` and `9. Mock-only controls` —
they use `/_test/` hooks and an `X-Mock-Status` header that exist only in the mock and **must not
be implemented** in production.

## What the real backend must do that the mock does not

These are hard requirements, not suggestions — see `API_SCHEMA.md` for detail.

1. **Verify IAP receipts** with the Google Play Developer API / Apple App Store Server API before
   granting anything on `POST /v1/iap/validate`. Dedupe by `transaction_id`; a replay returns 409
   (the client treats 409 as "already granted, safe to confirm").
2. **Verify social tokens** server-side with the provider on `POST /v1/auth/social`. The client
   never self-asserts identity.
3. **Persist everything.** The mock is in-memory and resets on restart.
4. **Decide the bootstrap coin-merge policy** (§2.1). The mock caps uploaded coins at 10 000.
5. **Own the win reward.** `POST /v1/game-results` returns `coins_granted` and the updated
   profile; the client applies whatever comes back and has no local constant. Any future
   watch-an-ad multiplier lives here too.
6. Rate-limit `POST /v1/game-results` — it is client-reported by design (accepted MVP risk, §7).
7. **Seed the challenge catalogue and keep lifetime rows out of the daily roll** (§7.1). There are
   25 challenges: 3 daily (regenerated each UTC day, progress resets) and 22 lifetime achievements
   with stable `lc_*` ids. If a lifetime row is regenerated or reset on the daily roll, targets like
   "Win 100 Games" become unachievable. The `challenges` table carries `type` and a NULLABLE
   `challenge_date` for exactly this reason.
8. **Score challenge progress server-side from match reports.** The client never submits progress,
   only outcomes; deriving it server-side is what stops every challenge being trivially forgeable.
   All 25 are now scorable. The nine `event_*` rules read the in-match fields the client adds to
   `POST /v1/game-results` (`events`, `jokers_played`, `duration_seconds`, `opponent_user_ids`,
   `forfeit`) — the server applies each rule's mode/win/rank condition (§7.1). Compare ranks from your
   own data before applying the result, never accept a rank from the client, and never let a forfeit
   satisfy a win-conditional rule.
9. **Extend the marble catalogue to 39 and deprecate 14 marbles** (§3 of `API_SCHEMA.md`). Until the
   live `GET /v1/shop/catalog` returns 39 marbles, the 14 new ones are unbuyable and any player who selects
   one is reset to the default on next login — the client clamps to the length it receives. Deprecated ids
   stay in the list with `is_active: false`; do not delete them.
10. **Implement `DELETE /v1/account`** (§1). The client's Settings → Delete Account calls it. Store
   policy requires in-app deletion for any app that creates accounts (Google Play also wants a web
   deletion link). Delete the user's data, revoke every session on every device, and unlink the guest
   device and social identities so the next login creates a new account. Keep IAP transaction ids.
   Until this endpoint exists the button shows "could not be deleted" and the player stays signed in.

## Conventions the client relies on (do not change without a client release)

- Envelope on every response: `{ "success", "data", "error": { "code", "message" } | null }`.
- `Authorization: Bearer <session_token>` on everything except the login endpoints; 401/403 makes
  the client drop its session and show the login screen.
- **Catalog item ids are permanent, dense and zero-based per category** (`0..N-1`); the client
  indexes its local prefab arrays by that exact integer. Append only; deprecate with `is_active: false`.
- The profile returned by login, bootstrap, `GET /v1/profile`, `iap/validate`, `daily-reward/claim`
  and `game-results` is applied **wholesale** by the client and overwrites local stats.
- `GET /v1/daily-challenges` returns **daily and lifetime challenges in one list**; the client
  renders a row per item and does not group them. Each carries `type` (`daily` | `lifetime`).
- `POST /v1/game-results` now carries **`game_mode`** (2, 4 or 6 — the match's seat count). It is
  required for the per-mode win challenges; a `0` means the report came from a client build older
  than this field.

## Contact

Client-side implementation of this contract: `Assets/Api/` in the Unity project (DTOs in `Dto/`,
calls in `Services/`). Those files are the source of truth for exact field names if this document
and the code ever drift.
