# API MVP Tiak-Tiak

Toutes les requetes sont en `POST` sur `/?e=nom_action`.

## Politique de version mobile

`app_version_policy` est public et doit être appelé au démarrage puis au retour du Store.

```json
{
  "app_code": "client",
  "platform": "ios",
  "current_version": "1.0.0",
  "current_build": 29
}
```

La réponse contient `policy.required`, le build/version minimum, le message et l’URL HTTPS du Store. Le build est comparé en priorité ; la version sémantique sert de repli lorsque le build installé n’est pas exploitable.

`admin_update_app_version_policies` requiert un jeton administrateur et accepte un tableau `policies`. L’activation de `is_enforced` est refusée tant que `store_url` est vide ou non HTTPS.

## Auth client

### `auth_send_otp`

```json
{
  "phone": "+221771234567",
  "terms_accepted": true,
  "terms_version": "2026-08-24"
}
```

### `auth_verify_otp`

```json
{
  "phone": "+221771234567",
  "otp": "482731",
  "zone": "Dakar",
  "terms_accepted": true,
  "terms_version": "2026-08-24"
}
```

Les clients qui affichent les conditions doivent envoyer les deux champs
`terms_accepted` et `terms_version`. Une acceptation explicitement fausse est
refusee. Pour conserver la compatibilite avec les anciennes versions de
l'application, une requete qui omet entierement ces deux champs reste acceptee
et aucune acceptation n'est alors enregistree.

Reponse:

```json
{
  "success": true,
  "data": {
    "token": "jwt...",
    "user": {
      "id": 1,
      "phone": "+221771234567",
      "zone": "Dakar",
      "role": "client",
      "active_role": "client",
      "available_roles": ["client", "driver"]
    }
  },
  "message": "Connexion reussie."
}
```

### `auth_switch_role`

Permet à un compte authentifié de changer d’espace sans se reconnecter. Le
token actif doit être envoyé dans l’en-tête `Authorization`. Le rôle client est
ajouté s’il manque. Lors du premier passage vers l’espace livreur, le profil
livreur et son rôle sont créés automatiquement sur le même `user_id`.

```json
{
  "role": "driver",
  "device_id": "android:identifiant-systeme",
  "device": {
    "platform": "android",
    "model": "SM-A155F"
  }
}
```

La reponse contient un nouveau token limite au role choisi ainsi qu'un profil
avec `active_role` et `available_roles`. Lors d’un passage vers le rôle
`driver`, l’appareil devient la seule session livreur active du compte. Les
protections de dette liée à l’appareil restent appliquées.

## Profil

### `user_profile`

Header:

```text
Authorization: Bearer <client_token>
```

### `user_update_profile`

```json
{
  "name": "Awa Diop",
  "email": "awa@example.com",
  "zone": "Dakar"
}
```

## Estimation

### `delivery_estimate_price`

```json
{
  "pickup_lat": 14.6928,
  "pickup_lng": -17.4467,
  "dropoff_lat": 14.7167,
  "dropoff_lng": -17.4677,
  "is_urgent": true,
  "is_remote_zone": false
}
```

## Livraison client

### `client_create_delivery`

Header client requis.

La creation publie immediatement une reservation en `searching_driver` pour un
paiement a la livraison. Pour Wave, Orange Money ou carte, elle utilise
`pending_payment`, puis passe en `searching_driver` apres confirmation du
paiement. Une nouvelle reservation n'est jamais creee en `draft`.

```json
{
  "pickup_address": "Plateau, Dakar",
  "pickup_lat": 14.667,
  "pickup_lng": -17.435,
  "dropoff_address": "Liberte 6, Dakar",
  "dropoff_lat": 14.735,
  "dropoff_lng": -17.473,
  "package_type": "document",
  "package_weight": "moins de 2kg",
  "description": "Envelope A4",
  "recipient_name": "Mamadou",
  "recipient_phone": "+221781112233",
  "pickup_instructions": "Appeler en arrivant",
  "dropoff_instructions": "Remettre au gardien",
  "payment_method": "wave"
}
```

Le backend construit et envoie le SMS destinataire apres la transaction de
creation. La livraison contient `recipient_sms_status` (`sent`, `failed` ou
`not_configured`) et `recipient_sms_error`. Un echec SMS ne duplique pas et
n'annule pas la commande. Le client peut appeler `client_resend_recipient_sms`
avec `delivery_id` pour relancer l'envoi.

Le SMS reste court : reference, expediteur, destination et code de remise a six
chiffres. Il invite ensuite le destinataire a telecharger l'application via
`APP_URL`. Il ne demande jamais sa position. Le livreur doit fournir le code
dans `recipient_otp` pour passer la livraison a `delivered`; le code attendu
est exposé uniquement à l'expéditeur et au destinataire authentifié.

### Partage de position du destinataire

`client_request_recipient_location` prend `recipient_name` et
`recipient_phone`, puis envoie un lien SMS securise valable 24 heures.
`client_recipient_location_status` prend `request_id` et retourne `pending`,
`shared` avec `lat`/`lng`, ou `expired`. La page publique envoie la position via
`recipient_share_location`; le jeton brut n'est jamais stocke en base.

### `client_confirm_delivery`

Sans `actor_role`, confirme la commande côté expéditeur (comportement historique).
Le destinataire authentifié confirme la réception avec :

```json
{
  "delivery_id": 1,
  "actor_role": "recipient"
}
```

Le serveur identifie le destinataire à partir du numéro de téléphone du compte
authentifié. Le numéro fourni par le client mobile n'est jamais utilisé pour
autoriser l'accès. La confirmation est possible à l'arrivée du livreur ou sur
une livraison déjà terminée, et renseigne `recipient_confirmed_at`.

### `client_cancel_delivery`

```json
{
  "delivery_id": 1,
  "reason": "Erreur adresse"
}
```

La réponse mobile reste limitée à la livraison annulée. Pour Wave ou Orange
Money confirmé, le backend calcule le montant net de la `platform_commission`
et crée une demande en attente dans le backoffice. Aucun cashout n'est lancé
automatiquement. Une confirmation reçue après l'annulation crée également cette
demande, sans transfert.

### `client_deliveries`

Sans corps, retourne l'historique des colis envoyés par le client. Pour obtenir
les colis qui lui sont adressés :

```json
{
  "scope": "recipient"
}
```

Les éléments retournés contiennent `viewer_role` (`sender` ou `recipient`).
Une livraison destinée au numéro du compte est reliée automatiquement, y
compris si le compte a été créé après la commande.

### `client_delivery_detail`

```json
{
  "delivery_id": 1
}
```

Le détail est accessible à l'expéditeur et au destinataire authentifié. Le code
de remise est renvoyé uniquement à ces deux parties autorisées afin d'être
affiché dans leur écran de suivi ; il reste absent des réponses livreur.
Après livraison, chacun peut noter le livreur une seule fois avec
`client_rate_driver`.

## Paiement

### `payment_create`

```json
{
  "delivery_id": 1,
  "method": "wave",
  "idempotency_key": "client-uuid-123"
}
```

Le moyen doit être celui enregistré sur la livraison. Le montant est toujours
lu côté serveur. Pour Wave et Orange Money, la réponse contient une URL Fayma :

```json
{
  "success": true,
  "data": {
    "payment": {"id": 14, "status": "pending", "provider_reference": "TTK-..."},
    "payment_type": "redirect",
    "payment_url": "https://...",
    "payment_intent_client_secret": null
  }
}
```

Pour la carte, le serveur demande en priorité le checkout hébergé Fayma afin de
retourner le même champ `payment_url`. `payment_intent_client_secret` reste un
retour de compatibilité fournisseur ; l’application ne doit jamais considérer
le paiement comme confirmé sans `client_verify_payment`.
Une nouvelle tentative doit utiliser une nouvelle clé d’idempotence ; le rejeu
de la même clé retourne la même transaction sans rappeler Fayma.

### `client_verify_payment`

```json
{
  "payment_id": 1
}
```

La réponse expose `confirmed` et `terminal`. Seul `confirmed: true` autorise
l’application à considérer le paiement comme acquis. Le client doit attendre le
webhook, par polling raisonnable ou via l’événement temps réel.

### `payment_webhook`

Endpoint appelé directement par Fayma. Le corps JSON brut est signé avec
`HMAC-SHA512(timestamp + corps)` en utilisant le secret marchand. La signature
doit être envoyée dans `Fayma-Signature` au format `t=<unix>,v1=<signature>`.
Les signatures âgées de plus de `FAYMA_WEBHOOK_TOLERANCE_SECONDS` sont refusées.
Le callback traite également les cashouts de remboursement `TTK-CR-*`.

```json
{
  "code": "TTK-ABC123",
  "status": "completed",
  "amount": 5000,
  "currency": "XOF"
}
```

## Livreur

### `driver_send_otp`

```json
{
  "phone": "+221770000001",
  "device_id": "android:identifiant-systeme",
  "device": {
    "installation_id": "uuid-installation",
    "platform": "android",
    "manufacturer": "Samsung",
    "model": "SM-A155F",
    "os_version": "14 (SDK 34)",
    "is_physical_device": true
  },
  "terms_accepted": true,
  "terms_version": "2026-08-24"
}
```

Lorsqu'ils sont fournis, `terms_accepted` et `terms_version` sont valides avant
l'envoi du SMS. Apres un envoi reussi, le serveur persiste la version, la date
et des empreintes SHA-256 de l'IP et du User-Agent. Les anciennes applications
qui omettent entierement ces champs restent compatibles.

Le meme numero peut utiliser les espaces client et livreur. Lors de la premiere
demande de code livreur, l'API cree un profil `pending` associe au compte (ou
cree le compte s'il n'existe pas). `DEVICE_COMMISSION_DUE` est l'unique motif
metier qui bloque la connexion livreur : en son absence, l'API reactive le
compte, approuve le profil et envoie le code, y compris pour un profil
`pending`, `suspended` ou archive. Le code expire apres 5 minutes.

### `driver_verify_otp`

```json
{
  "phone": "+221770000001",
  "otp": "593824",
  "zone": "Dakar",
  "device_id": "android:identifiant-systeme",
  "device": {
    "installation_id": "uuid-installation",
    "platform": "android",
    "manufacturer": "Samsung",
    "model": "SM-A155F",
    "os_version": "14 (SDK 34)",
    "is_physical_device": true
  }
}
```

Reponse:

```json
{
  "success": true,
  "data": {
    "token": "jwt-driver...",
    "driver": {
      "id": 2,
      "driver_id": 1,
      "phone": "+221770000001",
      "role": "driver",
      "active_role": "driver",
      "available_roles": ["client", "driver"]
    }
  }
}
```

### `driver_login`

```json
{
  "phone": "+221770000001",
  "password": "secret",
  "device_id": "android:identifiant-systeme",
  "device": {
    "platform": "android",
    "model": "SM-A155F"
  }
}
```

Une nouvelle connexion livreur devient immédiatement la seule session active
du compte. Les anciens jetons répondent HTTP 401 avec
`DEVICE_SESSION_REVOKED`; le compte garde néanmoins l’historique de tous ses
appareils. Si l’appareil a déjà été lié à un autre compte dont la commission
reste due, l’envoi ou la validation OTP répond HTTP 409 avec
`DEVICE_COMMISSION_DUE` et identifie le compte concerné :

```json
{
  "success": false,
  "reason": "DEVICE_COMMISSION_DUE",
  "message": "Une commission liee a ce numero doit etre soldee avant de creer ou utiliser un autre compte.",
  "data": {
    "phone": "+221770000011"
  }
}
```

Toutes les routes authentifiées du livreur exigent le même identifiant dans
l’en-tête `X-Device-ID`. Le backend ne conserve que son empreinte SHA-256.

### `driver_update_profile`

Header `Authorization: Bearer <driver_token>` :

```json
{
  "zone": "Dakar",
  "vehicle_type": "motorcycle",
  "vehicle_plate": "DK-1234-AA"
}
```

Le profil complet renvoye expose `driver_id` pour les relations internes et
`driver_identifier` pour l'affichage et l'assistance, par exemple
`TTK-LIV-000003`. Les types acceptes sont `motorcycle`, `cargo_tricycle`,
`car`, `van` et `truck`. Les anciennes applications qui envoient `tricycle`
ou `triporteur` restent compatibles ; le backend les normalise en
`cargo_tricycle`.

### `driver_profile`

Header `Authorization: Bearer <driver_token>`. Renvoie le profil livreur
complet, notamment `driver_identifier`, `vehicle_type`, `vehicle_plate`,
`verification_status` et `member_since`.

### `driver_documents`

Header livreur requis. Retourne au maximum une piece active par type :

```json
{
  "success": true,
  "data": {
    "documents": [
      {
        "id": 12,
        "document_type": "driving_licence",
        "status": "pending",
        "rejection_reason": null,
        "updated_at": "2026-08-24 12:00:00"
      }
    ],
    "onboarding": {
      "version": "guided_documents_v2",
      "ordered_types": ["identity_photo", "driving_licence", "vehicle_registration", "insurance"],
      "total_steps": 4,
      "completed_steps": 1,
      "current_step": 2,
      "next_document_type": "driving_licence",
      "is_complete": false
    }
  }
}
```

### `driver_upload_document`

Header livreur requis. Requete `multipart/form-data` :

- `document_type` : `driving_licence`, `insurance`,
  `vehicle_registration` ou `identity_photo` ;
- `document` : image JPEG, PNG, WebP, HEIC ou HEIF, 8 Mo maximum.
- `journey_version` : `guided_documents_v2` pour activer le parcours guide ;
- `journey_step` : numero de mission de 1 a 4 ;
- `document_verso` : requis avec le nouveau parcours pour le permis et la
  carte grise ;
- `liveness_detected=1` et `liveness_method=active_challenge_v1` : requis avec
  le nouveau parcours pour la photo d'identite.

Une nouvelle piece est creee avec le statut `pending`. Le remplacement d'une
piece rejetee reutilise son enregistrement, efface le motif de rejet, la repasse
a `pending` et supprime l'ancien fichier prive une fois la base mise a jour.
Le parcours versionne impose l'ordre photo d'identite, permis, carte grise,
assurance. Un appel historique sans `journey_version` reste accepte.
Le backoffice consulte les fichiers via `admin_document_file`, une route POST
reservee au role administrateur qui diffuse l'image avec `Cache-Control:
private, no-store` sans exposer `PRIVATE_STORAGE_PATH` au navigateur.

### `driver_update_availability`

```json
{
  "is_available": true
}
```

### `driver_update_location`

```json
{
  "lat": 14.7001,
  "lng": -17.4502,
  "heading": 120,
  "speed": 28
}
```

### `driver_available_deliveries`

Liste uniquement les livraisons ouvertes situees a 3 km maximum du point de
retrait. Le livreur doit etre approuve, disponible, localise et actif depuis
moins de 15 minutes. Une course refusee n'est plus proposee au meme livreur.

### `driver_accept_delivery`

La creation d'une offre ne renseigne pas `driver_id`. Le premier livreur
eligible qui accepte obtient atomiquement la course. Les offres envoyees aux
autres livreurs passent aussitot a `unavailable` et un evenement cible leur
demande de retirer la course de l'accueil.

```json
{
  "delivery_id": 1
}
```

### `driver_reject_delivery`

```json
{
  "delivery_id": 1
}
```

### `driver_update_delivery_status`

Statuts autorises:

- `accepted`
- `arriving_pickup`
- `picked_up`
- `arriving_dropoff`
- `delivered`
- `failed`

```json
{
  "delivery_id": 1,
  "status": "delivered",
  "proof_photo_path": "storage/uploads/proof-1.jpg",
  "recipient_otp": "839247"
}
```

### `driver_deliveries`

Historique livreur.

### `driver_earnings`

Retourne les revenus, le solde disponible et réservé, la commission due, les
mouvements, les 20 derniers cashouts et les 20 derniers règlements. Avant de
répondre, le portefeuille compense automatiquement les commissions non encore
réservées avec les gains disponibles. La compensation apparaît dans les
mouvements avec le type `commission_offset`. Un règlement Wave/Orange Money
encore en attente est toujours exclu de ce calcul afin d’éviter un double débit.

### `driver_request_cashout`

Crée un transfert Fayma sortant via `/transactions/cashout`. Le montant est réservé de façon
atomique avant l’appel fournisseur. En cas d’échec d’initiation ou de callback,
il est automatiquement recrédité au solde disponible. L’API accepte le numéro
au format local ou `+221`, le conserve en E.164, puis transmet uniquement les
9 chiffres locaux dans le champ `emeteur` pour les cashouts Wave et Orange Money.
Toute commission non réservée est déduite du solde avant de valider le montant
retirable.

```json
{
  "amount": 1500,
  "method": "wave",
  "phone": "+221770000001",
  "idempotency_key": "cashout-driver-1-attempt-42"
}
```

### `driver_settle_commission`

Crée un paiement Fayma (`type: 1`) pour régler la commission due. La réponse
contient `settlement`, `payment_url`, `confirmed` et `terminal`. La dette n’est
diminuée qu’après le callback Fayma signé.

### `driver_verify_commission_settlement`

```json
{
  "settlement_id": 18
}
```

Permet à Tiak-Tiak Pro de vérifier le paiement après le retour du checkout. Un
statut visuel ou la fermeture de Fayma ne remplace jamais la confirmation du
callback serveur. La page `/paiement/commission` tente automatiquement d’ouvrir le
scheme `tiaktiakpro://payment/commission`, avec un bouton de repli pour les
navigateurs intégrés qui bloquent l’ouverture sans interaction.

## FCM

### `fcm_register_device`

Header client ou livreur requis.

Le token est cree lors de la premiere connexion, actualise a chaque reprise de
session et reaffecte au dernier compte connecte sur l'appareil. Un meme compte
peut conserver plusieurs appareils, sans dupliquer un token FCM.

```json
{
  "fcm_token": "firebase-token",
  "platform": "android"
}
```

## Admin

### `admin_login`

```json
{
  "phone": "+221770000099",
  "password": "secret"
}
```

### `admin_deliveries`

Liste les commandes recentes.

### `admin_dashboard`

Retourne en une requete les indicateurs calcules, les commandes, les livreurs,
les documents, les paiements, l'activite recente et le tarif actif. Les valeurs
proviennent exclusivement de MySQL.

### `admin_dashboard_revision`

Retourne une empreinte legere des donnees operationnelles. Le dashboard la
verifie toutes les 5 secondes et recharge les listes uniquement lorsqu'une
action client, livreur ou administrateur a modifie la base.

### `admin_update_delivery`

Modifie les informations operationnelles d'une commande non finalisee :
destinataire, colis, adresses et instructions.

### `admin_create_delivery`

Crée une commande au nom d'un compte client actif depuis le backoffice. Le
corps reprend les champs de `client_create_delivery` et ajoute `user_id`. Le
tarif est calculé par le même moteur que dans l'app client ; un trajet de plus
de 20 km reste en étude interne et n'est pas diffusé aux livreurs avant sa
validation.

### `admin_archive_delivery` / `admin_restore_delivery`

Supprime une commande de la vue active sans effacer son historique, puis permet
sa restauration. Une commande en cours doit d'abord etre annulee ou terminee.

### `admin_assign_driver`

Affecte manuellement une course ouverte a un livreur approuve et actif. Les
offres envoyees aux autres livreurs sont rendues indisponibles immediatement.
Le livreur choisi recoit une notification et doit encore accepter la course
depuis son application.

```json
{
  "delivery_id": 1,
  "driver_id": 2
}
```

### `admin_cancel_delivery`

```json
{
  "delivery_id": 1,
  "reason": "Demande support"
}
```

### `admin_drivers`

Liste les livreurs.

### `admin_update_driver`

```json
{
  "driver_id": 2,
  "status": "approved"
}
```

### `admin_update_driver_profile`

Modifie le nom, le téléphone, l'email, la zone, la plaque et le permis du
livreur. Le numéro est normalisé au format sénégalais et son unicité est
contrôlée.

### `admin_update_user`

Modifie le nom, l'email et la zone d'un compte.

### `admin_ban_user`

```json
{
  "user_id": 12,
  "banned": true,
  "reason": "Abus confirme"
}
```

Le bannissement invalide immediatement les acces suivants et rend le livreur
indisponible. Un administrateur ne peut ni se bannir lui-meme ni bannir un autre
administrateur depuis cette interface.

### `admin_archive_user`

Archive ou restaure un compte sans supprimer les commandes, paiements et traces
associes. Les comptes clients avec une commande active et les livreurs assignés
à une course active ne peuvent pas être archivés.

### `admin_upload_document`

Ajoute ou remplace une pièce livreur depuis le backoffice. La requête est en
`multipart/form-data` avec `driver_id`, `type`, `document` et éventuellement
`document_verso`. Les types et
formats acceptés sont identiques à `driver_upload_document`. Tout remplacement
repasse la pièce à `pending` et conserve l'action dans le journal d'audit.

### `admin_update_document`

```json
{
  "document_id": 8,
  "status": "rejected",
  "reason": "Document expire"
}
```

### `admin_archive_document`

Masque ou restaure un document tout en conservant sa trace dans le journal
d'audit.

### `admin_confirm_cash`

Confirme un encaissement en especes uniquement lorsque la livraison est terminee.

```json
{
  "payment_id": 14
}
```

### `admin_refund_payment`

Déclenche un remboursement intégral auprès de Fayma. L’appel est réservé aux
administrateurs et journalisé.

```json
{
  "payment_id": 14,
  "reason": "Commande annulée"
}
```

### `admin_process_cancellation_refund`

Après vérification dans la section Paiements du backoffice, déclenche
manuellement le cashout Fayma (`type: 2`) du montant net vers le numéro ayant
payé. L'action est atomique, journalisée et ne peut pas être déclenchée deux fois.

```json
{
  "refund_id": 9,
  "phone": "+221781234567"
}
```

### `admin_update_pricing`

```json
{
  "name": "Tarif Dakar standard",
  "base_price": 500,
  "price_per_km": 150,
  "urgent_fee": 300,
  "remote_zone_fee": 500,
  "minimum_price": 1000,
  "driver_commission_percent": 75,
  "platform_commission_percent": 25
}
```
