> ## 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.

# Orders & Payment Endpoints (Cart, Orders, Konnect)

###### ***Orders API and Online Payment (Konnect) Guide***

This file documents the order-management endpoints exposed by `OrdersController`. It covers the cart/wishlist flow, the order lifecycle (creation, update, archive), assigning content for free, applying coupons, exporting invoices, and the full Konnect online-payment flow (init, webhook, history, public details page).

## **Table of Contents**

1. **Overview**
2. **Base URL and Headers**
3. **Order Model**
4. **Order Statuses**
5. **List Orders**
6. **Create Order**
7. **Show Order**
8. **Update Order Status / Notes**
9. **Update Order Notes Only**
10. **Archive Order**
11. **Unarchive Order**
12. **Add To Cart**
13. **Update Cart**
14. **Remove From Cart**
15. **Convert Cart To Order**
16. **Apply Coupon (Cart)**
17. **Unapply Coupon (Cart)**
18. **Assign Trainings To Students (Free)**
19. **Unassign Trainings From Student**
20. **Assign Exams To Students (Free)**
21. **Unassign Exams From Student**
22. **Export Order PDF (Single)**
23. **Export Orders PDF (Bulk ZIP)**
24. **Konnect — Overview**
25. **Konnect — Initiate Payment**
26. **Konnect — Webhook**
27. **Konnect — Payment Details (proxy)**
28. **Konnect — Order Webhook History**
29. **Konnect — Public Details Page (by paymentRef)**
30. **Internal Reused Methods**
31. **Known Implementation Notes**

***

## **1. Overview**

This file covers:

* the order data model (`App\Models\Order`) and its status table (`App\Models\OrderStatus`)
* the cart / wishlist / order type system (`type` column)
* the public list, create, show, update, archive, and unarchive endpoints
* the full cart workflow: `add_to_cart`, `update_cart`, `remove_from_cart`, `convert_cart_to_order`
* coupon apply / unapply on the cart
* free assignment / unassignment of trainings and exams to students
* PDF invoice exports (single and bulk)
* the Konnect online-payment integration: init, webhook, payment details proxy, history, and public details page

***

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

Base API URL:

```json theme={null}
{{base_url}}/api
```

Suggested Postman variables:

```json theme={null}
{{base_url}} = http://127.0.0.1:8000
{{token}} = your_sanctum_token
```

Headers:

```json theme={null}
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{token}}
```

For PDF exports use:

```json theme={null}
Accept: application/pdf
```

The Konnect public details endpoint and the webhook endpoint are public (no `Authorization` required), all other endpoints require Bearer token + the company-scope middleware.

***

## **3. Order Model**

The `App\Models\Order` model is backed by the `orders` table. Soft deletes and the `Archivable` trait are enabled.

### **Order** `type `**constants**

| **Constant** | **Value** | **Meaning** |
| :- | :- | :- |
| `Order::WISHLIST` | `1` | wishlist row, never paid |
| `Order::CART` | `2` | active cart for a student/company |
| `Order::ORDER` | `3` | finalized order (only this type goes paid) |

### **Fillable Fields**

| **Field** | **Type** | **Notes** |
| :- | :- | :- |
| `company_id` | integer | tenant scope |
| `student_id` | integer | the buyer (User) |
| `type` | integer | one of `WISHLIST(1)`, `CART(2)`, `ORDER(3)` |
| `order_status_id` | integer/null | foreign key to `order_status.id` — only meaningful when `type = ORDER` |
| `coupon_id` | integer/null | foreign key to `coupons.id` |
| `voucher_id` | integer/null | foreign key to `vouchers.id` |
| `notes` | string/null | manual notes (admin/instructor only) |
| `order_system_log` | text/null | append-only timeline (system events, free-assignment, coupon apply, etc.) |
| `access_granted_at` | datetime/null | when content access was granted |
| `created_at` | datetime | |
| `updated_at` | datetime | |

### **Auto-managed Fields**

| **Field** | **Notes** |
| :- | :- |
| `reference` | auto-generated on create, format `REF-XXXXXXXXXX` (10 random uppercase chars / digits) |
| `payment_ref` | populated by Konnect init, used to resolve orders in webhook (string) |
| `archived_at` | populated by `archive()`/`unarchive()` actions (Archivable trait) |
| `deleted_at` | populated when soft-deleted |

### **Appended Attributes**

| **Field** | **Notes** |
| :- | :- |
| `total_price` | sum of `order_items.price` across attached trainings + quizzes |
| `discounted_price` | `total_price` minus the linked coupon (`discount_percentage` or `discount_value`); falls back to `total_price` when no coupon is attached |

### **Relationships**

| **Relation** | **Type** | **Target** |
| :- | :- | :- |
| `company` | belongsTo | `Company` |
| `student` | belongsTo | `User` (foreign key `student_id`) |
| `order_status` | belongsTo | `OrderStatus` |
| `coupon` | belongsTo | `Coupon` |
| `voucher` | belongsTo | `Voucher` |
| `trainings` | belongsToMany | `Training` via `order_items` (`price`, `access_revoked`) |
| `quizzes` | belongsToMany | `Quiz` via `order_items` (`price`, `access_revoked`) |
| `konnectHistory` | hasOne | `KonnectHistory` (`order_id`) |

### **Side-effects (booted())**

* `creating`: generates a unique `reference` if missing, sets `created_at`/`updated_at`
* `updating`: refreshes `updated_at`
* `updated`: when `order_status_id` transitions into `PAID_ONLINE` or `PAID_OFFLINE` from a non-paid status, awards the student loyalty points based on `LoyaltyPointsSettings.key='points_per_tnd'`

### **Helper Methods**

* `sync_order_items(array $trainingIds, array $quizIds)` — replaces order items with the given lists, using each item's current `final_price`; updates the student's `owned_*` slug arrays for any added/removed slugs. Used by `convert_cart_to_order()`, `store()`, `update_cart()`.
* `assign_free_order_items(array $trainingIds, array $quizIds)` — attaches items at price `0`, without detaching existing items, and adds the slugs to the student's `owned_*` arrays. Used by `assign_trainings_to_students()`, `assign_exams_to_students()`, and `VoucherController::apply_voucher()`.

***

## **4. Order Statuses**

The `App\Models\OrderStatus` model is backed by the `order_status` table. Constants exposed:

| **ID** | **Constant** | **Label** | **Notes** |
| :- | :- | :- | :- |
| `1` | `OrderStatus::CANCELLED_ID` | `Cancelled` | terminal — emitted notification: "Order Cancelled" |
| `2` | `OrderStatus::PENDING_ID` | `Pending` | default after `convert_cart_to_order` (offline payment intent) |
| `3` | `OrderStatus::PAYMENT_FAILED_ID` | `Payment Failed` | set when Konnect returns failed/expired/canceled/refused |
| `4` | `OrderStatus::PAID_OFFLINE_ID` | `Paid Offline` | manually marked paid, also used by free-assignment endpoints |
| `5` | `OrderStatus::PAID_ONLINE_ID` | `Paid Online` | set by Konnect webhook on `completed` |
| `6` | `OrderStatus::PENDING_ONLINE_ID` | `Pending Online` | set when Konnect init succeeds, awaiting webhook |

### **Status table model**

```text theme={null}
{
  "id": 5,
  "label": "Paid Online",
  "description": "Paid via Konnect online checkout"
}
```

### **Status-driven business rules**

* `PENDING_ID` and `PENDING_ONLINE_ID` block the cart: items in those statuses cannot be re-added to a cart for the same student/company (HTTP 409 with `pending_conflicts`).
* `PAID_ONLINE_ID` and `PAID_OFFLINE_ID` are considered "paid". Updating an order into one of these triggers:
  * granting `owned_*` slugs to the student
  * de-duplicating items already in another paid order (and noting it in `order_system_log`)
  * awarding loyalty points (only on first transition into paid)
  * sending the "Order Accepted" notification
* `archive()` only accepts orders in: `PAID_OFFLINE`, `PAID_ONLINE`, `CANCELLED`, `PAYMENT_FAILED`.

***

## **5. List Orders**

Returns paginated finalized orders (`type = ORDER`) for a company.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/orders` |
| Auth | Bearer Token |

### **Query Parameters**

| **Name** | **Required** | **Notes** |
| :- | :- | :- |
| `company_id` | Yes | integer, must exist |
| `search` | No | string |
| `search_by_training` | No | boolean — when true, `search` matches `trainings.slug` / `trainings.label` |
| `search_by_exam` | No | boolean — when true, `search` matches `quizzes.slug` / `quizzes.label` |
| `search_by_student` | No | boolean — when true, `search` matches `student.email` |
| `search_by_instructor` | No | string — independent search by instructor first/last/full name or email on attached trainings/quizzes |
| `order_status` | No | integer; `0` = all |
| `order_status_id` | No | backward-compatible alias for `order_status` |
| `order_from` | No | date (start of day) |
| `order_to` | No | date (end of day), must be `>= order_from` |
| `student_id` | No | integer |
| `coupon_id` | No | integer |
| `type` | No | integer (override default `ORDER` filter) |
| `training_id` | No | filter orders containing one training |
| `quiz_id` | No | filter orders containing one quiz |
| `only_archived` | No | boolean — return only archived orders |
| `page` | No | default `1` |
| `per_page` | No | default `20`, max `100` |

> Only one of `search_by_training`, `search_by_exam`, `search_by_student` can be active at a time. When none is active, the default `search` matches against the order `reference`, `payment_ref`, or `student.email`.

### **Example Request**

```json theme={null}
GET /api/orders?company_id=1&order_status=5&page=1&per_page=10
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "current_page": 1,
  "data": [
    {
      "id": 401,
      "company_id": 1,
      "student_id": 22,
      "type": 3,
      "order_status_id": 5,
      "reference": "REF-ABCD123456",
      "payment_ref": "abc1234567890",
      "coupon_id": null,
      "voucher_id": null,
      "notes": null,
      "order_system_log": "[2026-02-12 10:30:00] Order passed by 'jane@x.com' with coupon 'N/A'",
      "created_at": "2026-02-12 10:30:00",
      "updated_at": "2026-02-12 10:31:42",
      "total_price": 120.5,
      "discounted_price": 120.5,
      "student": { "id": 22, "first_name": "Jane", "email": "jane@x.com", "image": null },
      "order_status": { "id": 5, "label": "Paid Online", "description": "..." },
      "trainings": [ { "id": 101, "label": "AI for PMs", "image": null, "pivot": { "price": 120.5, "access_revoked": false } } ],
      "quizzes": [],
      "coupon": null
    }
  ],
  "per_page": 10,
  "total": 1
}
```

***

## **6. Create Order**

Creates a finalized `ORDER` directly (skipping the cart). Items are attached using their current `final_price`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "company_id": 1,
  "student_id": 22,
  "order_status_id": 4,
  "notes": "Manual offline order",
  "trainings": [101, 102],
  "quizzes": []
}
```

* `trainings` is required when `quizzes` is empty (and vice-versa)
* IDs must be `distinct` and exist

### **Success Response**

```json theme={null}
{
  "order_id": 412,
  "message": "Order created successfully."
}
```

***

## **7. Show Order**

Returns one order with student/status/trainings/quizzes/coupon eager-loaded.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/order/{id}` |
| Auth | Bearer Token |

### **Query Parameters**

| **Name** | **Required** | **Notes** |
| :- | :- | :- |
| `search` | No | additionally filters by `reference` |

### **Example Request**

```json theme={null}
GET /api/order/401
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "id": 401,
  "company_id": 1,
  "student_id": 22,
  "type": 3,
  "order_status_id": 5,
  "reference": "REF-ABCD123456",
  "payment_ref": "abc1234567890",
  "coupon_id": null,
  "notes": null,
  "order_system_log": "...",
  "total_price": 120.5,
  "discounted_price": 120.5,
  "student": { "id": 22, "email": "jane@x.com", "image": null },
  "order_status": { "id": 5, "label": "Paid Online" },
  "trainings": [],
  "quizzes": [],
  "coupon": null,
  "created_at": "2026-02-12 10:30:00",
  "updated_at": "2026-02-12 10:31:42"
}
```

***

## **8. Update Order Status / Notes**

Updates `order_status_id` (and optional `notes`). When transitioning to a paid status, the controller:

* grants the student ownership of the order's items
* removes duplicates already paid in another order (recorded in `order_system_log`)
* awards loyalty points on first transition to paid
* sends an "Order Accepted" or "Order Cancelled" notification (after DB commit)

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{id}` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "order_status_id": 4,
  "notes": "Cash received at front desk"
}
```

### **Success Response**

```json theme={null}
{
  "message": "Order updated successfully."
}
```

***

## **9. Update Order Notes Only**

Admin / non-student endpoint to update only the `notes` field.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{id}/update-notes` |
| Auth | Bearer Token (admin/instructor) |

### **Request JSON**

```json theme={null}
{
  "notes": "Internal note: contacted student on 2026-02-13."
}
```

### **Success Response**

```json theme={null}
{
  "message": "Order notes updated successfully.",
  "order": { "id": 401, "notes": "...", "...": "..." }
}
```

### **Forbidden Response (student / instructor)**

```json theme={null}
{
  "error": "Unauthorized to modify order notes."
}
```

***

## **10. Archive Order**

Soft-archives an order. Only orders in `PAID_OFFLINE`, `PAID_ONLINE`, `CANCELLED`, or `PAYMENT_FAILED` status can be archived.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{id}/archive` |
| Alias URL | `/api/orders/{id}/archive` |
| Auth | Bearer Token |

### **Example Request**

```json theme={null}
POST /api/order/401/archive
Authorization: Bearer {{token}}
```

### **Success Response**

```json theme={null}
{
  "message": "Order archived successfully."
}
```

### **Already Archived / Wrong Status**

```json theme={null}
{
  "message": "Order is already archived."
}
```

```json theme={null}
{
  "message": "Only PAID_OFFLINE, PAID_ONLINE, CANCELLED, or PAYMENT_FAILED orders can be archived."
}
```

***

## **11. Unarchive Order**

Restores an archived order.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{id}/unarchive` |
| Alias URL | `/api/orders/{id}/unarchive` |
| Auth | Bearer Token |

### **Success Response**

```json theme={null}
{
  "message": "Order unarchived successfully.",
  "order": { "id": 401, "archived_at": null, "...": "..." }
}
```

***

## **12. Add To Cart**

Adds a single training **or** a single quiz to the student's cart. Creates the cart on first use.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/add_to_cart` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "company_id": 1,
  "student_id": 22,
  "training_id": 101
}
```

OR

```json theme={null}
{
  "company_id": 1,
  "student_id": 22,
  "quiz_id": 88
}
```

> Provide exactly one of `training_id` or `quiz_id`. The endpoint blocks the request when:
>
> * the same item is already in another `Pending` / `Pending Online` order (HTTP 409, includes `pending_conflicts`)
> * the item was previously unassigned from the student (HTTP 403)
> * the item is already in the cart (HTTP 403, `This training is already in your cart.` / `This exam is already in your cart.`)
>
> Every `message` of the cart and checkout endpoints (add / remove / convert to order, coupon format, Konnect init) is translated from the `Accept-Language` header (`en`, `fr`, `ar`; any other value falls back to `en`). The examples on this page are the English ones; `pending_conflicts` and the slugs inside the message are never translated.

### **Success Response**

```json theme={null}
{
  "order_id": 250,
  "message": "Cart updated successfully."
}
```

### **Pending Conflict Response**

```json theme={null}
{
  "message": "You already have these items in an order that is Pending or Pending Online. Please add a different item to your cart then proceed. Trainings: [ai-for-pm]",
  "pending_conflicts": {
    "trainings": [ { "id": 101, "slug": "ai-for-pm", "label": "AI for PMs" } ],
    "quizzes": []
  }
}
```

***

## **13. Update Cart**

Replaces the entire cart (or wishlist) contents in one call.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/update_cart` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "company_id": 1,
  "student_id": 22,
  "type": 2,
  "trainings": [101, 102],
  "quizzes": [88],
  "coupon_id": 5
}
```

* `type` is `2` for cart, `1` for wishlist
* `trainings` and `quizzes` are required (each `min:1`)

### **Success Response**

```json theme={null}
{
  "order_id": 250,
  "message": "Cart updated successfully."
}
```

### **Pending Conflict Response (HTTP 409)**

Same shape as `add_to_cart` (with `pending_conflicts`).

***

## **14. Remove From Cart**

Removes a single training or quiz from the student's cart. If the cart becomes empty and has no coupon, it is deleted.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/remove_from_cart` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "student_id": 22,
  "training_id": 101,
  "company_id": 1
}
```

> Provide exactly one of `training_id` or `quiz_id`. `company_id` is optional but recommended on multi-tenant deployments.

### **Success Response (cart still has items)**

```json theme={null}
{
  "order_id": 250,
  "message": "Cart updated successfully."
}
```

### **Success Response (cart deleted)**

```json theme={null}
{
  "order_id": null,
  "message": "Cart updated successfully."
}
```

### **Not Found Response**

```json theme={null}
{
  "message": "No active cart found for that student."
}
```

***

## **15. Convert Cart To Order**

Converts the student's cart into a finalized `ORDER` with status `PENDING`. The cart is detached, items are migrated to the new order via `sync_order_items`, and an "Order Received" notification is sent (after DB commit).

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/convert_cart_to_order/{student_id}` |
| Auth | Bearer Token |

### **Example Request**

```shellscript theme={null}
POST /api/order/convert_cart_to_order/22
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "order_id": 412,
  "message": "Order created successfully."
}
```

### **Empty Cart Response**

```json theme={null}
{
  "message": "Your shopping cart is currently empty. Please add at least one training or exam to proceed with your order."
}
```

### **Pending Conflict Response (HTTP 409)**

```json theme={null}
{
  "message": "You already have these items in an order that is Pending or Pending Online. ...",
  "pending_conflicts": { "trainings": [...], "quizzes": [...] }
}
```

***

## **16. Apply Coupon (Cart)**

Applies a coupon code to the student's active cart.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/cart/{student_id}/apply_coupon` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "code": "WELCOME50"
}
```

* code must be uppercase letters and digits only
* coupon must exist, be `active`, in its date window, have remaining `max_usage`, and meet `applicable_if_total_is_above`
* the coupon cannot have been previously applied/used by this student

### **Success Response**

```json theme={null}
{
  "message": "Coupon applied successfully.",
  "coupon_code": "WELCOME50",
  "cart": { "id": 250, "...": "..." }
}
```

### **Common Error Responses (HTTP 400/403)**

```json theme={null}
{ "message": "Please add an item to your cart before applying a coupon." }
{ "message": "Coupon already applied to this cart." }
{ "message": "You have used this coupon before." }
{ "message": "Coupon is not active." }
{ "message": "Coupon has expired." }
{ "message": "Total purchase does not meet the threshold for this coupon." }
{ "message": "Coupon usage limit reached." }
```

***

## **17. Unapply Coupon (Cart)**

Removes the currently applied coupon from the student's cart and refunds one usage to the coupon.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/cart/{student_id}/unapply_coupon` |
| Auth | Bearer Token |

### **Success Response**

```json theme={null}
{
  "message": "Coupon unapplied successfully.",
  "cart": { "id": 250, "...": "..." }
}
```

### **No Coupon Applied Response**

```json theme={null}
{
  "message": "No coupon is applied to this cart."
}
```

***

## **18. Assign Trainings To Students (Free)**

Bulk-creates `PAID_OFFLINE` orders for one or more students, attaching the listed trainings at price `0` and granting ownership.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/assign_trainings_to_students` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "company_id": 1,
  "students": [22, 23],
  "trainings": [101, 102]
}
```

### **Success Response**

```json theme={null}
{
  "message": "Trainings assigned successfully."
}
```

***

## **19. Unassign Trainings From Student**

Bulk-creates `PAID_OFFLINE` orders to record the unassignment, then flips `access_revoked = true` on the original `order_items` rows and removes the slugs from the student's `owned_trainings`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/unassign_trainings_from_student` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "students": [22, 23],
  "trainings": [101, 102]
}
```

### **Success Response**

```json theme={null}
{
  "message": "Trainings unassigned successfully."
}
```

***

## **20. Assign Exams To Students (Free)**

Same as section 18, but for standalone exams (quizzes whose `training_id` and `chapter_id` are both `null`). Quizzes that are linked to a training or chapter are rejected.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/assign_exams` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "company_id": 1,
  "students": [22, 23],
  "exams": [88, 89]
}
```

### **Success Response**

```json theme={null}
{
  "message": "Exams assigned successfully."
}
```

### **Validation Error (non-standalone exam)**

```json theme={null}
{
  "message": "The following IDs are not standalone exams and cannot be assigned: 90, 91"
}
```

***

## **21. Unassign Exams From Student**

Mirror of section 19, but for exams.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/unassign_exams` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "students": [22],
  "exams": [88]
}
```

### **Success Response**

```json theme={null}
{
  "message": "Exams unassigned successfully."
}
```

***

## **22. Export Order PDF (Single)**

Renders the order as a PDF invoice (`pdf_order_template` blade) and returns it inline.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/orders/{id}/export-pdf` |
| Auth | Bearer Token |

### **Example Request**

```shellscript theme={null}
GET /api/orders/401/export-pdf
Authorization: Bearer {{token}}
Accept: application/pdf
```

### **Success Behavior**

* returns `Content-Type: application/pdf`
* file name pattern: `order-{reference}.pdf` (or `order-{id}.pdf` if no reference)

***

## **23. Export Orders PDF (Bulk ZIP)**

Renders multiple orders as PDFs and returns them as a ZIP download. Per-order errors are written into the ZIP as `order-{id}-ERROR.txt`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/orders/export-pdf-bulk` |
| Auth | Bearer Token |

### **Request JSON**

```json theme={null}
{
  "order_ids": [401, 402, 403]
}
```

### **Success Behavior**

* returns a ZIP download named `orders-export-{uuid}.zip`
* the temporary ZIP file is deleted after sending

***

## **24. Konnect — Overview**

Konnect is the online-payment gateway. The flow is:

1. **Init** — `POST /api/order/{user_id}/konnect/init` flips the student's CART to ORDER + `PENDING_ONLINE`, then calls Konnect's `/init`. The response contains `payUrl` (open in browser to pay) and `paymentRef` (stored on the order).
2. **Pay** — the student opens `payUrl`, completes payment on Konnect.
3. **Webhook** — Konnect calls `GET /api/konnect/webhook?payment_ref=...`. The controller fetches the payment from Konnect, updates `order_status_id`, grants ownership on `completed`.
4. **Inspection** — `GET /api/konnect/payments/{paymentId}` proxies to Konnect for raw status; `GET /api/order/{id}/konnect-history` returns the local webhook trace; `GET /order/konnect-payment-details/{paymentRef}` is a public details page (HTML or `?json=1`).

### **Required Environment Variables**

| **Var** | **Notes** |
| :- | :- |
| `KONNECT_INIT_URL` | Konnect `/init` endpoint |
| `KONNECT_URL` | base URL for Konnect payment lookup (`/payments/...`) |
| `KONNECT_API_KEY` | API key sent in the `x-api-key` header |
| `KONNECT_WALLET_ID` | receiver wallet id used as `receiverWalletId` in init payloads |
| `MAIL_USERNAME` | shown as the platform contact email in invoices/details page |

### **Money Scale**

* The init endpoint converts `discounted_price` to **millimes** before sending to Konnect (`amount = round(discounted_price * 1000)`).
* The DB stores prices in TND (the controller's `priceScale = 1000`).

### **KonnectHistory Model**

Backed by table `konnect_history` (note: singular).

| **Field** | **Notes** |
| :- | :- |
| `order_id` | one-to-one with `Order` |
| `webhook_history` | append-only text trace of Konnect events for this order (one entry/line) |

Two private helpers append lines:

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

***

## **25. Konnect — Initiate Payment**

Resolves the student's CART (or returns 404 if none), flips it to ORDER + `PENDING_ONLINE`, generates a Konnect payment, persists `payment_ref`, and sends a customizable notification to the student with the `payUrl`.

| **Field** | **Value** |
| :- | :- |
| Method | `POST` |
| URL | `/api/order/{user_id}/konnect/init` |
| Auth | Bearer Token |

### **Request JSON (all optional)**

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

* `notif_email` — boolean, when `true` the notification is also emailed
* `notif_type_id` — defaults to `NotificationType::SUCCESS_ID`

### **Example Request**

```shellscript theme={null}
POST /api/order/22/konnect/init
Authorization: Bearer {{token}}
Content-Type: application/json
Accept: application/json

{}
```

### **Success Response**

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

### **No Cart Response**

```json theme={null}
{
  "message": "No active cart or payable order found for this user."
}
```

### **Empty Cart Response**

```json theme={null}
{
  "message": "Your shopping cart is empty. Please add at least one item before payment."
}
```

### **Zero Amount Response**

```json theme={null}
{
  "message": "Order amount must be greater than 0 for online payment."
}
```

### **Konnect Init Failure (HTTP 502)**

```json theme={null}
{
  "message": "The online payment could not be started. Please try again later."
}
```

***

## **26. Konnect — Webhook**

Public endpoint hit by Konnect (or by the success page after redirect). Resolves the order via `payment_ref`, fetches the live payment from Konnect, applies the corresponding status change, and grants ownership on `completed`.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/konnect/webhook?payment_ref={ref}` |
| Auth | Public (no Bearer token) |

> Content negotiation:
>
> * pass `?json=1` (or `Accept: application/json`) to get JSON
> * pass `?view=1` or no `Accept` for HTML (renders `konnect_payment_page_success` / `konnect_payment_page_pending` / `konnect_payment_page_failed`)

### **Status Mapping**

| **Konnect status** | **Order status set** | **HTTP** |
| :- | :- | :- |
| `completed` | `PAID_ONLINE_ID` (5) | `200` |
| `pending`, `created` | `PENDING_ONLINE_ID` (6) | `202` |
| `canceled` / `cancelled` / `failed` / `expired` / `refused` | `PAYMENT_FAILED_ID` (3) | `402` |
| any other value | unchanged | `402` |

> When the order is already paid, the endpoint returns idempotently with `200` and `"Order already paid."`.

### **Example Request (JSON)**

```shellscript theme={null}
GET /api/konnect/webhook?payment_ref=abc1234567890&json=1
Accept: application/json
```

### **Success Response (completed)**

```json theme={null}
{
  "message": "Your payment has been processed successfully!",
  "konnect_status": "completed",
  "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 Response (HTTP 202)**

```json theme={null}
{
  "message": "Your payment is pending on Konnect. It may take up to 20 minutes to verify your bank transaction. ...",
  "konnect_status": "pending",
  "order_status_id": 6,
  "order_status_label": "Pending Online",
  "payment_ref": "abc1234567890",
  "order_id": 412,
  "order_reference": "REF-ABCD123456",
  "student_email": "jane@x.com"
}
```

### **Failure Response (HTTP 402)**

```json theme={null}
{
  "message": "Payment failed.",
  "konnect_status": "failed",
  "order_status_id": 3,
  "order_status_label": "Payment Failed",
  "payment_ref": "abc1234567890",
  "order_id": 412,
  "order_reference": "REF-ABCD123456",
  "student_email": "jane@x.com"
}
```

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

```json theme={null}
{ "message": "Invalid payment_ref format." }
{ "message": "Order not found for this payment_ref." }
```

***

## **27. Konnect — Payment Details (proxy)**

Proxies a `GET /payments/{paymentId}` call to Konnect and returns their raw JSON. Useful in Postman to inspect a payment's status manually.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/konnect/payments/{paymentId}` |
| Auth | Bearer Token |

### **Example Request**

```shellscript theme={null}
GET /api/konnect/payments/abc1234567890
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response (raw Konnect payload)**

```json theme={null}
{
  "payment": {
    "id": "abc1234567890",
    "status": "completed",
    "amount": 120500,
    "token": "TND",
    "details": { "...": "..." }
  }
}
```

### **Validation / Konnect Error Pass-through**

```json theme={null}
{ "message": "Invalid paymentId format." }
{ "message": "Failed to fetch Konnect payment details." }
```

> Konnect 4xx errors (e.g., 404 for unknown payment) are passed through with the same status code and body.

***

## **28. Konnect — Order Webhook History**

Returns the Konnect history trace stored locally for one order.

| **Field** | **Value** |
| :- | :- |
| Method | `GET` |
| URL | `/api/order/{id}/konnect-history` |
| Auth | Bearer Token |

### **Example Request**

```shellscript theme={null}
GET /api/order/412/konnect-history
Authorization: Bearer {{token}}
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "konnect_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] Konnect init requested; about to POST /init\n[2026-02-12 10:30:02] Konnect init ok; paymentRef=abc1234567890\n[2026-02-12 10:35:11] Webhook hit; payment_ref=abc1234567890\n[2026-02-12 10:35:12] Konnect webhook completed; payment_ref=abc1234567890\n"
}
```

### **Empty History Response**

```json theme={null}
{
  "konnect_webhook_history": ""
}
```

***

## **29. Konnect — Public Details Page (by paymentRef)**

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

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

### **Example Request (JSON)**

```shellscript theme={null}
GET /order/konnect-payment-details/abc1234567890?json=1
Accept: application/json
```

### **Success Response**

```json theme={null}
{
  "order": {
    "id": 412,
    "reference": "REF-ABCD123456",
    "status_id": 5,
    "status_label": "Paid Online",
    "student_email": "jane@x.com"
  },
  "invoice": {
    "items": [
      {
        "type": "Training",
        "slug": "ai-for-pm",
        "instructor": "instructor@x.com",
        "price": 120.5,
        "revoked": false
      }
    ],
    "subtotal": 120.5,
    "discount": 0,
    "total": 120.5
  },
  "konnect": {
    "ok": true,
    "status": "completed",
    "raw": { "payment": { "id": "abc1234567890", "status": "completed", "...": "..." } },
    "error": null,
    "payment_ref": "abc1234567890"
  },
  "webhook_history": "Order passed by 'jane@x.com' with coupon 'N/A' at 2026-02-12 10:30:00\n[2026-02-12 10:35:12] Konnect webhook completed; payment_ref=abc1234567890\n"
}
```

### **Order Not Found / Konnect Error**

* When the local order is not found, `order` is `null` but the page still renders the live Konnect data and a minimal invoice shell.
* When Konnect itself fails the request, `konnect.ok` is `false` and `konnect.error` describes the failure (e.g., `"Payment not found on Konnect."`).

### **Validation Error**

```json theme={null}
{
  "message": "Invalid payment_ref format."
}
```

***

## **30. Internal Reused Methods**

These methods are part of `OrdersController` but do not have their own route:

* `findPendingConflicts(int $studentId, int $companyId, array $trainingIds, array $quizIds): array` — returns trainings/quizzes already attached to a `PENDING` or `PENDING_ONLINE` order for the same student/company. Used by `add_to_cart()`, `update_cart()`, `convert_cart_to_order()`.
* `pendingConflictMessage(array $conflicts): string` — formats the human message for the conflict response.
* `getThemeForCompany(int $companyId): array` — loads platform branding (logo, name) for invoice PDFs.
* `buildOrderPdfViewData(Order $order): array` — gathers the data passed to the `pdf_order_template` blade.
* `appendKonnectHistoryRaw(Order $order, string $rawLine): void` — appends a non-timestamped line to `konnect_history.webhook_history`.
* `historyFromWebhook(Order $order, string $action, ?string $note = null): void` — appends a timestamped `[date] action; note` line.
* `konnectViewName(string $status): string` — picks the Blade view for the webhook HTML response (`konnect_payment_page_success` / `konnect_payment_page_pending` / `konnect_payment_page_failed`).

The model also exposes:

* `Order::sync_order_items(array $trainingIds, array $quizIds)` — see section 3.
* `Order::assign_free_order_items(array $trainingIds, array $quizIds)` — see section 3.

***

## **31. Known Implementation Notes**

* `index()` defaults to `type = ORDER`; carts and wishlists are excluded unless a `type` query parameter is sent explicitly.
* The cart auto-deletes itself in `remove_from_cart()` when empty AND no coupon is attached, so subsequent calls to "show the cart" can return nothing legitimately.
* `convert_cart_to_order()` deletes the source cart after migrating items so there are no "ghost carts" left on the user.
* Loyalty points are awarded twice in code (in the `Order::booted()` `updated` hook and in `update()`'s duplicate-removal path). The hook uses `LoyaltyPointsSettings.key='points_per_tnd'`; `update()` sums `loyalty_points` per training/quiz.
* `initiate_konnect_payment()` requires a CART; it does not create one. Make sure `add_to_cart` (or `update_cart`) was called first.
* `konnect_webhook()` is publicly reachable and resolves orders strictly by `payment_ref`. The legacy `{id}` path segment is preserved but ignored.
* `orderKonnectDetailsByRef()` is **public** (declared outside the `auth:sanctum` group) and can be browsed without a token — anyone with the `paymentRef` can see the invoice-style page.
* Konnect's `KONNECT_URL`, `KONNECT_INIT_URL`, `KONNECT_API_KEY`, and `KONNECT_WALLET_ID` must be configured in `.env`. The amount is always sent in millimes.
* Several Konnect-related routes are commented out in `routes/api.php` (e.g. older variants of the init route). The currently active routes are listed above.


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