# HerbaCommunity API — integration notes

These files are **application code only**. They are meant to be dropped into a
fresh Laravel skeleton, not to run on their own (there is no `vendor/`,
`artisan`, `composer.json` or `bootstrap/` here — those come from Laravel).

> **Nothing in this folder has been executed.** There was no PHP runtime on the
> machine when it was written, so it has not been linted, migrated or tested.
> Treat the first `php artisan migrate` as the real first test.

---

## 1. Create the skeleton

```bash
cd <the HerbaCommunity folder>          # wherever you copied it
composer create-project laravel/laravel herbacommunity-api
cd herbacommunity-api
php artisan install:api                 # Laravel 11+: creates routes/api.php + Sanctum
```

## 2. Copy these files in

Copy the contents of `api\` over the new project, preserving paths:

```
app\Models\*                     app\Scopes\BranchScope.php
app\Models\Concerns\*            app\Services\*
app\Http\Controllers\Api\*       app\Policies\*
app\Http\Middleware\SetActiveBranch.php
database\migrations\*            routes\api.php
```

**Delete the skeleton's own users migration** —
`database/migrations/0001_01_01_000000_create_users_table.php` — ours replaces
it (and also creates `sessions` + `cache`, so remove the stock cache migration
too if present).

## 3. Register the middleware alias

`bootstrap/app.php`:

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'active.branch' => \App\Http\Middleware\SetActiveBranch::class,
    ]);
    // Sessions are needed because SetActiveBranch stores the active club there.
    $middleware->api(prepend: [
        \Illuminate\Session\Middleware\StartSession::class,
    ]);
})
```

If you would rather stay fully stateless, swap the two `session(...)` calls in
`SetActiveBranch` and `BranchScope` for a request-scoped singleton — the header
`X-Branch-Id` already carries the club on every request.

## 4. Policies

Laravel 11+ auto-discovers `App\Policies\{Model}Policy`. `MemberPolicy`,
`BranchPolicy` and `GuestPolicy` follow that convention, so no registration is
needed. Controllers call `$this->authorize(...)`, which needs
`Illuminate\Foundation\Auth\Access\AuthorizesRequests` on the base
`App\Http\Controllers\Controller` — add it if the skeleton's base class is empty
(it is, on Laravel 11+):

```php
abstract class Controller
{
    use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
}
```

## 5. Database

`.env`:

```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=herbacommunity
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
```

```bash
php artisan migrate
php artisan db:seed        # once the seeders are written — see task 6
php artisan serve
```

---

## Things to check on first run

These are the spots most likely to need a small fix, since none of it has been
executed yet:

1. **`Branch::nextCardId()` uses `SUBSTRING(card_id, 5)`** — MySQL-specific, and
   assumes the `HBL-` prefix is exactly 4 characters. On SQLite (if you use it
   for tests) this needs `substr(card_id, 5)`.
2. **`PersonMatchService` phone matching on `members`** uses a raw `RIGHT(REPLACE(...))`
   expression because members store the raw phone. If that proves slow at volume,
   add a `phone_normalised` column to `members` too and index it — same as guests.
   That is the better long-term shape; it was left off only to avoid changing the
   member import path in the same pass.
3. **`UniqueConstraintViolationException`** exists from Laravel 10.x. On older
   versions catch `Illuminate\Database\QueryException` and check the SQLSTATE.
4. **Session-based active branch** means the API is not stateless. If you put it
   behind a load balancer later, move sessions to Redis or switch to the header.

---

## Security decisions worth keeping

- **PINs are hashed** (`pin_hash`), rate-limited per user+IP, with lockout. The
  prototype compared them in client-side JS.
- **`BranchScope` fails closed** — no session, or a branch it cannot resolve,
  yields `WHERE 1 = 0` rather than every row.
- **The club switcher is validated server-side.** `SetActiveBranch` only accepts
  a target inside the user's own subtree; a tampered `X-Branch-Id` silently falls
  back to the home club.
- **Recharge amounts are computed on the server** from `price_history`. The
  client never sends a price.
- **The guest duplicate check is the one deliberate scope bypass**, via the
  explicit `acrossNetwork()` helper so it is greppable. It returns only club,
  date and time — never another club's member list.
- **Repeat guests cannot be logged silently**: the server re-runs the check and
  returns `409` unless `acknowledge_repeat=true` is sent, and the acknowledgement
  is written to `activity_logs`.
