# Sayfet (Laravel)

Peer-to-peer parcel marketplace between Europe and Morocco. Carriers post trips
(with intermediary stops, price/kg, capacity, and accepted/refused categories);
senders book a slot. Carriers can save reusable Routes and schedule trips from
them by picking just a departure datetime.

No online payments in the MVP — money changes hands in person on handoff.

This is the Laravel + MySQL rebuild of the original Next.js + Postgres
prototype.

## Stack

- Laravel 12 + PHP 8.2 (Blade SSR, no Node toolchain)
- MySQL 8 (or MariaDB 10.11+)
- Built-in session auth (credentials + signed magic link)
- 7 locales: FR / AR / EN / ES / DE / DA / SV (full RTL for Arabic)
- MinIO (S3-compatible) via the AWS SDK / Flysystem S3
- Mailpit for dev email
- Twilio Verify for phone OTP (dev fallback writes the code to logs)

## Quick start (Docker)

```bash
cp .env.example .env
docker compose up -d
# Migrations and seeds run automatically on first boot.
```

Open:

- App: <http://localhost:8000/fr>
- MinIO console: <http://localhost:9001> (sayfet / sayfetsayfet)
- Mailpit: <http://localhost:8025>

Seed accounts:

| Role    | Email                   | Password      |
|---------|-------------------------|---------------|
| Admin   | admin@sayfet.local      | admin12345    |
| Carrier | carrier@sayfet.local    | carrier12345  |
| Sender  | sender@sayfet.local     | sender12345   |

## Local dev (without Docker)

Requires PHP 8.2+ with the standard extensions (`pdo_mysql`, `mbstring`,
`intl`, `gd`, `zip`, `bcmath`, `openssl`, `curl`) and a running MySQL.

```bash
composer install
cp .env.example .env
php artisan key:generate
# Set DB_* in .env to point at your local MySQL, then:
php artisan migrate --seed
php artisan serve
```

## Tests

```bash
./vendor/bin/phpunit
```

The unit suite covers the `RouteService::schedule` offset math — converting
a saved Route's per-stop offsets into concrete `TripStop` arrival windows on
top of a chosen `departure_at` — which is the most non-obvious piece of
domain logic.

## Project layout

```
database/migrations/          # all schema (users, profiles, trips/stops, routes/stops, bookings, messages, reviews, reports, blocks)
database/seeders/             # admin + carrier + sender + sample route + sample trip
app/Models/                   # Eloquent models (ULID primary keys)
app/Services/                 # domain logic (TripService, RouteService, BookingService, ReviewService, OtpService, StorageService)
app/Http/Controllers/         # request handlers (carrier/, admin/, auth/, etc.)
app/Http/Middleware/          # SetLocale, EnsureRole, EnsureNotBlocked
resources/views/              # Blade templates (locale-prefixed)
lang/{fr,ar,en,es,de,da,sv}/  # translation bundles
routes/web.php                # all routes, locale-prefixed under /{locale}/...
public/css/app.css            # single self-contained stylesheet (no Node/Vite)
docker-compose.yml            # mysql + minio + mailpit + app
```

## Notable behaviours

- **Stop-aware search**: a sender filtering "Lyon → Casablanca" matches a
  Paris→Casablanca trip that lists Lyon as an intermediary stop. The booking
  form lets the sender pick the pickup/dropoff stop.
- **Saved Route → Trip**: `RouteService::schedule(carrierId, routeId, departureAt)`
  materialises a Trip + TripStops by adding each stop's offset to the chosen
  departure. Editing the Trip never mutates the saved Route.
- **Phone reveal**: phone numbers stay hidden until the carrier approves the
  booking (status `APPROVED`).
- **Carrier KYC**: carriers must upload an ID document (stored on S3/MinIO);
  admins approve in `/{locale}/admin/carriers`. Only `APPROVED` carriers can
  publish trips through the UI.
- **i18n**: 7 locales wired with proper `lang` and `dir` attributes. FR/AR/EN
  fully translated; ES/DE/DA/SV start as copies of EN — translate
  `lang/{locale}/*.php` to localise.
- **Capacity accounting**: approving/cancelling/rejecting bookings credits
  and debits `trips.remaining_capacity_kg` inside a transaction with a row
  lock.
