# Sales Marathon — Backend & Admin Web App

A real-time sales competition system. Admins create date-limited marathons,
select salespeople, and manually update lead scores. The leaderboard updates
live over websockets, and the marathon locks automatically when it ends with
the final Top 3 stored permanently.

**Stack:** Laravel 12 · PHP 8.3 · MySQL 8 · Sanctum · Reverb (websockets) ·
Laravel Queue · Scheduler · Firebase Cloud Messaging.

The Flutter mobile app lives in a separate project:
`D:\flutter-projects\sales_marathon_app`.

> **Setting this up on a new PC or server?** Read **[SETUP.md](SETUP.md)** —
> it covers the backend and the mobile app end to end, from a fresh clone.

---

## Requirements

- PHP 8.3+ with `openssl`, `pdo_mysql`
- Composer
- MySQL 8+ (window functions are used for ranking)

## Setup

```bash
composer install
copy .env.example .env
php artisan key:generate
```

Create the databases (adjust credentials in `.env` first if needed):

```sql
CREATE DATABASE sales_marathon;
CREATE DATABASE sales_marathon_test;  -- used by the test suite
```

Then migrate and seed demo users:

```bash
php artisan migrate --seed
```

### Seeded accounts (password: `password`)

| Role | Email |
|---|---|
| Admin | admin@salesmarathon.test |
| Salesperson | ahmed@salesmarathon.test |
| Salesperson | mohamed@salesmarathon.test |
| Salesperson | hassan@salesmarathon.test |
| Salesperson | ali@salesmarathon.test |
| Salesperson | yusuf@salesmarathon.test |
| Salesperson | fatima@salesmarathon.test |

## Running (development)

Four processes are needed. Start them all at once with the bundled
launcher (no Node required):

```bash
.\start-dev
```

PowerShell needs the leading `.\`; in cmd.exe plain `start-dev` works.
`.\stop-dev` stops everything.

`composer dev` does the same thing but needs Node.js, since it shells out to
`npx concurrently`.

...or run them individually:

```bash
php artisan serve            # HTTP  -> http://127.0.0.1:8000
php artisan reverb:start     # websockets -> ws://127.0.0.1:8080
php artisan queue:work       # notifications, lifecycle jobs
php artisan schedule:work    # starts/finalizes marathons every minute
```

Open **http://127.0.0.1:8000** and log in as the admin.

### Testing from a physical phone

`php artisan serve` binds to `127.0.0.1`, which a phone cannot reach. Bind it
to every interface instead, and make sure the phone is on the same Wi-Fi:

```bash
php artisan serve --host=0.0.0.0 --port=8000
```

The Reverb server already listens on `0.0.0.0`, so no change is needed there.
Build the mobile app pointing at this machine's LAN IP (see the Flutter
project README) and allow ports 8000/8080 through the Windows firewall if the
phone cannot connect.

> In production run the queue workers under Supervisor and register
> `php artisan schedule:run` in cron every minute.

## Resetting test data

```bash
php artisan marathons:reset
```

Deletes all marathons, participants, score history, results, notifications
and pending jobs, and restarts ids at 1 — while keeping every user account.
Useful between rounds of manual testing. Flags: `--force` (no prompt),
`--device-tokens` (also clear FCM tokens), `--keep-ids`.

## Push notifications (FCM)

Optional locally — without it, pushes are skipped but in-app notifications
still work. To enable:

1. Firebase console → Project settings → Service accounts →
   *Generate new private key* (a JSON file).
2. Point `FIREBASE_CREDENTIALS` in `.env` at the file's absolute path and
   set `FIREBASE_PROJECT_ID`.

Delivery uses the FCM HTTP v1 API. Stale device tokens are removed
automatically when FCM reports them unregistered.

## Architecture notes

- **Raw SQL + bindings.** All application queries live in `app/Queries/*`
  as parameterized SQL (`DB::select/insert/update/delete`), per project
  requirements. Eloquent models exist for relationships, casts, and auth.
- **Business logic** is in `app/Actions/*` — one class per operation
  (create marathon, add participants, update score, start, finalize).
- **Score updates** run in a transaction with `SELECT ... FOR UPDATE` row
  locks; every change is recorded in the `score_updates` audit table.
  Setting the same score is a no-op; decreasing requires the explicit
  `allow_decrease` confirmation flag.
- **Ranking:** `current_score DESC`, tie broken by earliest
  `score_reached_at` (participants who never scored use `joined_at`).
- **Lifecycle:** the scheduler runs `marathons:check-lifecycle` every
  minute; it queues idempotent `StartMarathonJob` / `FinalizeMarathonJob`.
  Finalization snapshots the full ranking into `marathon_results`
  (`prize_position` 1–3 for the podium) and locks the marathon.
- **Realtime:** events broadcast on the private channel `marathon.{id}`
  (`score.updated`, `marathon.started`, `marathon.completed`,
  `participant.added`). Channel auth allows admins and that marathon's
  participants. Auth endpoint: `POST /api/broadcasting/auth` (session or
  Bearer token via Sanctum).

## API overview

All endpoints are JSON under `/api` and require `Authorization: Bearer <token>`
(from `POST /api/auth/login`) except login itself. The admin web UI uses the
same API with its session cookie (Sanctum stateful).

```text
POST   /api/auth/login                {email, password, device_name?}
POST   /api/auth/logout
GET    /api/auth/me

GET    /api/marathons/active          active marathon + leaderboard + my entry
GET    /api/marathons/upcoming
GET    /api/marathons/history         completed marathons
GET    /api/marathons/{id}            details + final results (if completed)
GET    /api/marathons/{id}/leaderboard
GET    /api/me/dashboard              my rank, gap to leader, history
GET    /api/me/notifications
PATCH  /api/me/notifications/{id}/read
POST   /api/me/device-tokens          {token, platform}
DELETE /api/me/device-tokens/{token}

GET    /api/admin/marathons
POST   /api/admin/marathons           {name, description?, start_at, end_at, participant_user_ids[]}
GET    /api/admin/marathons/{id}
PATCH  /api/admin/marathons/{id}
POST   /api/admin/marathons/{id}/cancel
GET    /api/admin/marathons/{id}/participants
POST   /api/admin/marathons/{id}/participants          {user_ids[]}
DELETE /api/admin/marathons/{id}/participants/{pid}
POST   /api/admin/marathons/{id}/participants/{pid}/leads   {leads}            # add leads
PATCH  /api/admin/marathons/{id}/participants/{pid}/score   {score, allow_decrease?}  # correct total
GET    /api/admin/marathons/{id}/score-updates
GET    /api/admin/marathons/{id}/results

GET    /api/admin/salespeople         ?search=   active only, for the pickers
GET    /api/admin/salespeople/manage  ?search=   includes deactivated
POST   /api/admin/salespeople         {name, email, password}
PATCH  /api/admin/salespeople/{id}    {name, email, password?}
PATCH  /api/admin/salespeople/{id}/active  {is_active}
```

### Scoring: add vs. correct

Day to day the admin **adds** the leads someone just won
(`POST .../leads` with `{"leads": 3}`). The new total is computed
server-side inside the row lock, so two admins adding at the same moment
stack instead of overwriting each other — the client never sends a total.

`PATCH .../score` remains for **corrections**: it overwrites the running
total outright and requires `allow_decrease` to lower it. Both paths write
the same `score_updates` audit record.

## Tests

120 feature tests cover authentication, marathon rules, participant
management, score updates + audit, ranking/tie-breaks, finalization
idempotency, notifications, device tokens, and authorization. They run
against the `sales_marathon_test` MySQL database:

```bash
php artisan test
```
