> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Online Payment System - Flouci

###### ***Flouci Online-Payment Integration Guide***

This file documents the **online-payment** flow powered by **Flouci** (a Tunisian payment gateway). It is a focused companion to [Orders & payment](https://theteam-13fc7f49.mintlify.site/api-reference/orders-and-payment-endpoints.md) (the broader order/cart system) and zooms in on everything related to paying for an order online: initiating a payment, the callback/webhook that confirms it, the online payment statuses, the inspection endpoints, history tracing, and the public details page.

> **Migration note:** this gateway **replaces Konnect** (decommissioned). The endpoint shapes were kept deliberately close to the old Konnect ones, so frontends only need to swap the URL prefix `konnect` → `flouci` and read the renamed `flouci_status` field. The order column `orders.payment_ref` now stores the Flouci `payment_id`.

All endpoints live in `App\Http\Controllers\OrdersController`. Amounts are handled in **millimes** (1 TND = 1000 millimes).

***

## **Table of Contents**

1. **Overview & End-to-End Flow**
2. **Base URL and Headers**
3. **Environment Configuration**
4. **Money / Amount Scale**
5. **Online Payment Statuses**
6. `payment_ref `**/** `payment_id `**& Order Resolution**
7. **Endpoint — Initiate Payment**
8. **Endpoint — Webhook / Callback (status confirmation)**
9. **Endpoint — Verify Payment (proxy)**
10. **Endpoint — Order Webhook History**
11. **Endpoint — Public Payment Details Page**
12. **Flouci History Model**
13. **Notifications on Init**
14. **Idempotency & Ownership Grant**
15. **Error Reference**
16. **Implementation Notes & Gotchas**

***

## **1. Overview & End-to-End Flow**

Flouci is the **online** alternative to the offline / free-assignment payment paths. The lifecycle is:

```text theme={null}
   Student                    Elearny API                        Flouci
     |                            |                                  |
     | 1. add_to_cart ...         |                                  |
     |--------------------------->|  (cart built — see Orders doc)   |
     |                            |                                  |
     | 2. POST flouci/init        |                                  |
     |--------------------------->|  flip CART → ORDER (Pending Online)
     |                            |  POST /generate_payment -------> |
     |                            |  <--- { link, payment_id } -----  |
     |  <--- pay_url + payment_ref|  store payment_id on order        |
     |                            |  send "payment link" notification |
     |                            |                                  |
     | 3. open pay_url (link)     |                                  |
     |-------------------------------------------------------------->|
     |  (pay with wallet / card)                                     |
     |                            |                                  |
     | 4. Flouci redirects + webhook                                 |
     |                            |  GET /flouci/webhook?payment_id=..|
     |                            |<---------------------------------|
     |                            |  GET /verify_payment/{id} ------> |
     |                            |  <--- { success, result.status }  |
     |                            |  map status → order_status_id     |
     |                            |  grant ownership if "SUCCESS"     |
     |                            |                                  |
     | 5. inspect (optional)      |                                  |
     |  GET flouci-history        |                                  |
     |  GET flouci-payment-details (public)                          |
```

**Steps:**

1. **Build the cart** — `add_to_cart` / `update_cart` (see [Orders & payment](https://theteam-13fc7f49.mintlify.site/api-reference/orders-and-payment-endpoints.md)). Init does **not** create a cart; it requires one to already exist.
2. **Init** — `POST /api/order/{user_id}/flouci/init` flips the student's `CART` row in place to an `ORDER` with status `PENDING_ONLINE`, calls Flouci's `/generate_payment`, stores the returned `payment_id` on the order (`payment_ref` column), and returns `pay_url` (Flouci's `link`) + `payment_ref`.
3. **Pay** — the student opens `pay_url` and completes payment on Flouci's checkout.
4. **Webhook / redirect** — Flouci redirects the browser to (and pings the webhook with) `GET /api/flouci/webhook?payment_id=...`. The API looks up the order by the stored `payment_id`, **verifies** the live payment via Flouci's `/verify_payment/{id}`, maps its status to an order status, and grants content ownership on `SUCCESS`.
5. **Inspect** — the verify proxy, history, and public-details endpoints let you read the live and stored payment state.

***

## **2. Base URL and Headers**

Base API URL: `{{base_url}}/api`.

| **Endpoint** | **Auth** |
| :- | :- |
| `POST /order/{user_id}/flouci/init` | Bearer + company middleware |
| `GET /flouci/payments/{paymentId}` | Bearer + company middleware |
| `GET /order/{id}/flouci-history` | Bearer + company middleware |
| `GET /flouci/webhook` | **Public** (no Bearer) |
| `GET /order/flouci-payment-details/{ref}` | **Public** (no Bearer) |

> The webhook and the public details page are intentionally unauthenticated — Flouci's servers (and the browser redirect) call the webhook, and the details page is shareable by `payment_id`. Anyone holding the `payment_id` can view the invoice-style details page.

***

## **3. Environment Configuration**

These must be set in `.env`:

| **Var** | **Notes** |
| :- | :- |
| `FLOUCI_PUBLIC_KEY` | App public key from the Flouci developer dashboard |
| `FLOUCI_PRIVATE_KEY` | App private/secret key |
| `FLOUCI_GENERATE_URL` | Generate endpoint (default `https://developers.flouci.com/api/v2/generate_payment`) |
| `FLOUCI_VERIFY_URL` | Verify endpoint base; controller appends `/{payment_id}` (default `.../api/v2/verify_payment`) |
| `FLOUCI_SUCCESS_LINK` | Optional browser redirect after a successful payment; defaults to the public webhook page |
| `FLOUCI_FAIL_LINK` | Optional browser redirect after a failed payment; defaults to the public webhook page |
| `MAIL_USERNAME` | Shown as the platform contact email on invoices / details page |

**Authentication:** every Flouci call sends the header `Authorization: Bearer <FLOUCI_PUBLIC_KEY>:<FLOUCI_PRIVATE_KEY>`.

### **Flouci** `/generate_payment `**payload (sent server-side)**

```text theme={null}
{
  "amount": "120500",                 // millimes, as a string (see §4)
  "success_link": "https://your-host/api/flouci/webhook",
  "fail_link":    "https://your-host/api/flouci/webhook",
  "webhook":      "https://your-host/api/flouci/webhook",
  "developer_tracking_id": "REF-ABCD123456",  // order reference
  "session_timeout_secs": 1200,
  "accept_card": true
}
```

Flouci appends `?payment_id=...` to `success_link`, `fail_link`, and `webhook`.

***

## **4. Money / Amount Scale**

* Flouci expects **millimes** for TND (1 TND = 1000 millimes; e.g. `1.500 TND` → `1500`), sent as a **string**.
* The init endpoint computes the amount from the order's appended `discounted_price`:

```text theme={null}
priceScale = 1000  (hard-coded in initiate_flouci_payment)
amount     = round(discounted_price * 1000)   // TND → millimes
```

* The amount must be `> 0`, otherwise init returns **HTTP 422** (`"Order amount must be greater than 0 for online payment."`).
* `discounted_price` already reflects any applied coupon.

***

## **5. Online Payment Statuses**

The Flouci flow uses four of the six `OrderStatus` values (`App\Models\OrderStatus`):

| **ID** | **Constant** | **Label** | **When set in the Flouci flow** |
| :- | :- | :- | :- |
| `3` | `OrderStatus::PAYMENT_FAILED_ID` | `Payment Failed` | Flouci status is `FAILURE` / `EXPIRED` |
| `5` | `OrderStatus::PAID_ONLINE_ID` | `Paid Online` | Flouci status is `SUCCESS` — **ownership granted here** |
| `6` | `OrderStatus::PENDING_ONLINE_ID` | `Pending Online` | Set at init; also on Flouci `PENDING` |
| `2` | `OrderStatus::PENDING_ID` | `Pending` | (offline intent) — accepted as a payable order before init |

### **Flouci → Order status mapping (webhook)**

The webhook only trusts the response when the verify call returns top-level `success: true`.

| **Flouci** `result.status` | **Order status set** | **HTTP** |
| :- | :- | :- |
| `SUCCESS` | `PAID_ONLINE_ID` (5) | `200` |
| `PENDING` | `PENDING_ONLINE_ID` (6) | `202` |
| `FAILURE`, `EXPIRED` | `PAYMENT_FAILED_ID` (3) | `402` |
| `success:false` or unknown status | unchanged | `402` |

> A `PENDING` webhook never downgrades an order that is already `PAID_ONLINE`.

***

## **6.** `payment_ref `**/** `payment_id `**& Order Resolution**

* On a successful init, Flouci returns a `payment_id`, which the API persists on `orders.payment_ref`.
* **Every subsequent Flouci interaction is resolved by that id.** The webhook accepts `payment_id` (Flouci's query-param name) or `payment_ref` (alias) and resolves `Order::where('payment_ref', $id)`.
* The id must match `^[A-Za-z0-9_-]+$`. Anything else → **HTTP 422** `"Invalid payment_id format."`.

***

## **7. Endpoint — Initiate Payment**

Resolves the student's `CART`, flips it in place to an `ORDER` with `PENDING_ONLINE`, calls Flouci `/generate_payment`, stores the `payment_id`, and notifies the student with the pay link.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{user_id}/flouci/init` |
| Auth | Bearer Token (+ company middleware) |
| Policy | `view` on the resolved order |

### **Request JSON (all optional — controls the notification)**

```text theme={null}
{
  "notif_title":   "Payment link ready 🎉",
  "notif_message": "Custom message override...",
  "notif_email":   true,
  "notif_type_id": 1
}
```

### **Success Response (HTTP 200)**

```text theme={null}
{
  "pay_url": "https://flouci.com/pay/abc1234567890",
  "payment_ref": "abc1234567890",
  "message": "Flouci payment initiated.",
  "order_id": 412,
  "order_reference": "REF-ABCD123456",
  "student_email": "jane@x.com",
  "order_status_id": 6,
  "order_status_label": "Pending Online"
}
```

### **Error Responses**

```text theme={null}
// 404 — no cart
{ "message": "No active cart or payable order found for this user." }
// 400 — cart exists but empty
{ "message": "Your shopping cart is empty. Please add at least one item before payment." }
// 422 — amount not positive
{ "message": "Order amount must be greater than 0 for online payment." }
// 502 — malformed Flouci response (missing link / payment_id)
{ "message": "Flouci init: unexpected response." }
// 502 — Flouci call threw / network error
{ "message": "Flouci init failed." }
```

***

## **8. Endpoint — Webhook / Callback (status confirmation)**

Public endpoint hit by **Flouci** after the student pays — both as the browser redirect (`success_link` / `fail_link`) and the server-to-server webhook. Resolves the order by `payment_id`, **verifies** the live payment with Flouci, maps the status, persists it, and grants ownership on `SUCCESS`.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/flouci/webhook?payment_id={id}` |
| Auth | **Public** (no Bearer token) |
| Route name | `flouci.webhook` |

### **Query Parameters**

| **Name** | **Required** | **Notes** |
| :- | :- | :- |
| `payment_id` | Yes | the Flouci id stored on the order (`^[A-Za-z0-9_-]+$`). `payment_ref` is accepted as an alias. |
| `json` | No | `1` forces a JSON response |
| `view` | No | `1` forces an HTML (Blade) response |

### **Content Negotiation**

* `?json=1` **or** `Accept: application/json` → JSON payload.
* `?view=1` or a browser (no JSON `Accept`) → an HTML page:
  * `SUCCESS` → `flouci_payment_page_success`
  * `PENDING` → `flouci_payment_page_pending`
  * anything else → `flouci_payment_page_failed`

### **Behavior**

1. Reads `payment_id` (or `payment_ref`) and validates it (`^[A-Za-z0-9_-]+$`).
2. Looks up the order by stored id. If none → **404**.
3. **Idempotency:** if the order is already `PAID_ONLINE` or `PAID_OFFLINE`, returns immediately with **200** and `"Order already paid."` (no Flouci call, no re-grant).
4. Otherwise `GET`s `FLOUCI_VERIFY_URL/{payment_id}`, checks `success == true`, reads `result.status`, maps it (see §5), saves the order, and on `SUCCESS` calls `grantOwnedAssetsFromOrder($order)`.

### **Success Response — SUCCESS (HTTP 200)**

```text theme={null}
{
  "message": "Your payment has been processed successfully!",
  "flouci_status": "SUCCESS",
  "order_status_id": 5,
  "order_status_label": "Paid Online",
  "payment_ref": "abc1234567890",
  "order_id": 412,
  "order_reference": "REF-ABCD123456",
  "student_email": "jane@x.com"
}
```

### **Pending (HTTP 202) / Failure (HTTP 402)**

Same payload shape with `flouci_status` = `PENDING` (202) or `FAILURE` / `EXPIRED` (402). Already-paid hits return **200** with `"Order already paid."` and `flouci_status: "SUCCESS"`.

### **Validation / Not Found / Server Errors**

```text theme={null}
// 422
{ "message": "Invalid payment_id format." }
// 404
{ "message": "Order not found for this payment_id." }
// 500 — Flouci verify network failure
{ "message": "Flouci server error." }   // trans('messages.flouci_server_error')
```

***

## **9. Endpoint — Verify Payment (proxy)**

Proxies a `GET /verify_payment/{paymentId}` to Flouci and returns their raw JSON. Useful for manually inspecting a payment's status (e.g. in Postman).

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/flouci/payments/{paymentId}` |
| Auth | Bearer Token (+ company middleware) |

### **Success Response (raw Flouci payload, pass-through status)**

```text theme={null}
{
  "success": true,
  "result": {
    "type": "wallet",
    "amount": 120500,
    "status": "SUCCESS",
    "details": { "...": "..." },
    "settlement_status": "AVAILABLE"
  }
}
```

### **Errors**

```text theme={null}
// 422
{ "message": "Invalid paymentId format." }
// 500
{ "message": "Failed to fetch Flouci payment details." }
```

> Flouci 4xx errors (e.g. 404 for an unknown payment) are passed through with their original status code and body.

***

## **10. Endpoint — Order Webhook History**

Returns the locally stored history trace for one order (append-only log of init + webhook events).

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/order/{id}/flouci-history` |
| Auth | Bearer Token (+ company middleware) |

### **Success Response**

```text theme={null}
{
  "flouci_webhook_history": "Order passed by 'jane@x.com' with coupon 'N/A' at 2026-02-12 10:30:00\n[2026-02-12 10:30:01] Flouci init requested; about to POST /generate_payment\n[2026-02-12 10:30:02] Flouci init ok; paymentId=abc1234567890\n[2026-02-12 10:35:11] Webhook hit; payment_ref=abc1234567890\n[2026-02-12 10:35:12] Flouci webhook completed; payment_id=abc1234567890\n"
}
```

***

## **11. Endpoint — Public Payment Details Page**

Combines the local order/invoice data, a **live** Flouci verify lookup, and the stored webhook history. Renders an HTML page by default; pass `?json=1` for JSON. **Public** — no Bearer token.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/order/flouci-payment-details/{paymentRef}` |
| Route name | `api.order.flouci.details` |
| Auth | **Public** |

### **Success Response (JSON)**

```text theme={null}
{
  "order": { "id": 412, "reference": "REF-ABCD123456", "status_id": 5, "status_label": "Paid Online", "student_email": "jane@x.com" },
  "invoice": { "items": [ ... ], "subtotal": 120.5, "discount": 0, "total": 120.5 },
  "flouci": {
    "ok": true,
    "status": "SUCCESS",
    "raw": { "success": true, "result": { "status": "SUCCESS", "amount": 120500, "...": "..." } },
    "error": null,
    "payment_ref": "abc1234567890"
  },
  "webhook_history": "Order passed by 'jane@x.com' ...\n[2026-02-12 10:35:12] Flouci webhook completed; payment_id=abc1234567890\n"
}
```

### **Partial / Error States**

* **Order not found locally:** `order` is `null`, but the page still renders the live Flouci data and a minimal invoice shell.
* **Flouci lookup failed:** `flouci.ok` is `false` and `flouci.error` describes the failure (e.g. `"Payment not found on Flouci."`).

***

## **12. Flouci History Model**

Backed by table `flouci_history` (note: **singular**). One row per order via `Order::flouciHistory()` (`hasOne`).

| **Field** | **Notes** |
| :- | :- |
| `order_id` | one-to-one with `Order` |
| `webhook_history` | append-only text trace, one event per line |

Two private controller helpers append to it:

* `appendFlouciHistoryRaw($order, $rawLine)` — appends a **raw** line (no timestamp prefix). Used for the human `"Order passed by … with coupon …"` line.
* `historyFromWebhook($order, $action, $note = null)` — appends a timestamped `[YYYY-mm-dd HH:ii:ss] {action}; {note}` line.

> This trace is **separate** from the global audit log (`insert_in_history_table(...)`); both may be written during the Flouci flow.

***

## **13. Notifications on Init**

After a successful init, a notification is sent to the student carrying the pay link (via `NotificationService::toUser`):

* **Default title:** `"Payment link ready 🎉"`.
* **Default message:** embeds the order reference, the amount (`X.XXX TND`), the `pay_url`, and a "check status anytime" hint.
* **Email:** sent when `notif_email` is truthy (default `true`).
* **Type:** `notif_type_id` (default `NotificationType::SUCCESS_ID`).
* **Failure-safe:** if the notification fails, the init flow still succeeds; the failure is logged and traced in the Flouci history.

***

## **14. Idempotency & Ownership Grant**

* **Idempotent webhook:** once an order is `PAID_ONLINE` / `PAID_OFFLINE`, repeated webhook hits short-circuit with **200** `"Order already paid."` — no second Flouci verify and no double-grant.
* **Ownership is granted at** `SUCCESS`**,** not at init. The webhook calls `student->grantOwnedAssetsFromOrder($order)`, which pushes the order's training/exam **slugs** into the student's `owned_trainings` / `owned_exams`.
* **Defensive cleanup:** `sanitizeOwnedAssets()` runs at init, and on `SUCCESS` / failure to strip any stale ownership predating the paid-gating guard.
* Loyalty points are awarded on the first transition into a paid status (via the `Order::booted()` `updated` hook).

***

## **15. Error Reference**

| **HTTP** | **Endpoint** | **Message** | **Cause** |
| :- | :- | :- | :- |
| `400` | init | `Your shopping cart is empty...` | cart exists but has no items |
| `404` | init | `No active cart or payable order found for this user.` | no `CART` row for the user |
| `422` | init | `Order amount must be greater than 0 for online payment.` | `discounted_price` ≤ 0 |
| `502` | init | `Flouci init: unexpected response.` | Flouci response missing `link` / `payment_id` |
| `502` | init | `Flouci init failed.` | Flouci call threw / timed out |
| `422` | webhook | `Invalid payment_id format.` | id not `^[A-Za-z0-9_-]+$` |
| `404` | webhook | `Order not found for this payment_id.` | no order with that id |
| `202` | webhook | pending message | Flouci `PENDING` |
| `402` | webhook | `Payment failed.` | Flouci `FAILURE` / `EXPIRED` / unverifiable |
| `500` | webhook | `Flouci server error.` | Flouci verify network failure |
| `422` | verify proxy | `Invalid paymentId format.` | id not `^[A-Za-z0-9_-]+$` |
| `500` | verify proxy | `Failed to fetch Flouci payment details.` | Flouci call threw |

***

## **16. Implementation Notes & Gotchas**

* **Init requires a pre-existing** `CART`**.** It does not create one — call `add_to_cart` / `update_cart` first.
* **The CART row is flipped in place** to an `ORDER` (`Pending Online`) — no new row is created.
* `payment_id `**(stored in** `payment_ref`**) is the source of truth.** The webhook and details endpoints resolve by it; Flouci sends it as the `payment_id` query param.
* **Webhook is public** — never embed secrets in it; treat it as internet-reachable.
* **Public details page is browsable** by anyone holding the `payment_id`.
* **Amounts are always millimes**, sent as a string; the init endpoint hard-codes `priceScale = 1000` (DB in TND → millimes).
* **Only trust verified payments:** the webhook requires top-level `success == true` **and** `result.status == "SUCCESS"` before granting ownership.
* **Two history trails:** the Flouci-specific `flouci_history.webhook_history` (human-readable trace) and the global audit log (`insert_in_history_table`).

***

> **Related docs:** [Orders & payment](https://theteam-13fc7f49.mintlify.site/api-reference/orders-and-payment-endpoints.md) (full cart/order/coupon/assignment flow — Flouci is summarized there too), [Notifications](https://theteam-13fc7f49.mintlify.site/api-reference/notifications.md) (notifications), [Archive, delete & restore flow](https://theteam-13fc7f49.mintlify.site/api-reference/archive-delete-and-restore-flow.md) (order archive lifecycle).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.