# BERNIE Backend — API Reference for Frontend

**Base URL:** `http://localhost:8000/api`

All API endpoints return JSON. The API uses **session-based authentication** via cookies for cart/basket/orders. No JWTs or tokens are needed for the public storefront.

---

## Products

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/products` | List all active products |
| `GET` | `/api/products/trending` | Trending products (limit 10) |
| `GET` | `/api/products/search?q=` | Search products by keyword |
| `GET` | `/api/products/{slug}` | Get single product by slug |
| `POST` | `/api/products` | Create product |
| `PUT` | `/api/products/{id}` | Update product |
| `DELETE` | `/api/products/{id}` | Delete product |

**Product response (`GET/POST/PUT`):** all product endpoints return data wrapped in the `data` envelope:

```json
{
  "data": {
    "id": 1,
    "name": "Organic Bananas",
    "slug": "organic-bananas",
    "description": "Fresh organic bananas sourced from sustainable farms.",
    "price": 2.99,
    "unit": "bunch",
    "unit_price": 0.3,
    "image_url": "http://localhost:8000/storage/media/products/abc123.jpg",
    "brand": null,
    "discount": 10,
    "original_price": 3.32,
    "final_price": 2.69,
    "is_on_sale": true,
    "nutri_score": "A",
    "tags": ["organic", "fruit", "fresh"],
    "nutrition": null,
    "preparation": null,
    "ingredients": null,
    "allergens_info": null,
    "stock_quantity": 50,
    "min_order_quantity": 1,
    "max_order_quantity": null,
    "is_active": true,
    "is_save_the_food": false,
    "is_weekly_special": true,
    "category_id": 1,
    "category": {
      "id": 1,
      "name": "Obst & Gemüse",
      "slug": "obst-gemuese",
      "icon": "apple",
      "image_url": null
    },
    "media": [
      {
        "id": 1,
        "url": "http://localhost:8000/storage/media/products/abc123.jpg",
        "filename": "bananas.jpg",
        "mime_type": "image/jpeg"
      }
    ],
    "created_at": "2026-05-24T10:00:00.000000Z",
    "updated_at": "2026-05-24T10:00:00.000000Z"
  }
}
```

**List endpoints** (`GET /api/products`, `trending`, `search`) return an array:

```json
{
  "data": [ { ... }, { ... } ]
}
```

**Input fields for `POST`/`PUT`:**

| Field | Type | Required |
|-------|------|----------|
| `name` | string | yes |
| `slug` | string | yes |
| `price` | numeric | yes |
| `description` | string | no |
| `unit` | string | no |
| `unit_price` | numeric | no |
| `image` | file (jpg/png/gif/webp/svg, max 2MB) | no |
| `category_id` | integer | no |
| `brand` | string | no |
| `discount` | numeric (0–100) | no |
| `original_price` | numeric | no |
| `tags` | array | no |
| `nutri_score` | string | no |
| `nutrition` | array | no |
| `ingredients` | string | no |
| `is_save_the_food` | boolean | no |
| `is_weekly_special` | boolean | no |

`image_url` is computed automatically from uploaded media — do not send it manually.

---

## Categories

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/categories` | List all active categories |
| `GET` | `/api/categories/{slug}` | Get category by slug |
| `POST` | `/api/categories` | Create category |
| `PUT` | `/api/categories/{id}` | Update category |
| `DELETE` | `/api/categories/{id}` | Delete category |

**Category response:**

```json
{
  "data": {
    "id": 1,
    "name": "Obst & Gemüse",
    "slug": "obst-gemuese",
    "icon": "apple",
    "description": null,
    "image_url": null,
    "sort_order": 1,
    "is_active": true,
    "media": [],
    "created_at": "2026-05-24T10:00:00.000000Z",
    "updated_at": "2026-05-24T10:00:00.000000Z"
  }
}
```

**Input fields for `POST`/`PUT`:**

| Field | Type | Required |
|-------|------|----------|
| `name` | string | yes |
| `slug` | string | yes |
| `image` | file (jpg/png/gif/webp/svg, max 2MB) | no |
| `icon` | string | no |
| `description` | string | no |
| `sort_order` | integer | no |
| `is_active` | boolean | no |

---

## Sections

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/sections` | List all active sections |
| `GET` | `/api/sections/{key}` | Get section by key (e.g. `hero`, `save-the-food`, `trending`) |
| `POST` | `/api/sections` | Create section |
| `PUT` | `/api/sections/{id}` | Update section |
| `DELETE` | `/api/sections/{id}` | Delete section |

**Section response (array):**

```json
[
  {
    "id": 1,
    "key": "hero",
    "title": "Fresh From Local Farmers",
    "subtitle": "Summer Seasonal",
    "description": "Direct delivery to your doorstep within 3 hours.",
    "image_url": "https://...",
    "button_text": "Shop Harvest",
    "button_link": "/categories",
    "badge_text": "Summer Seasonal",
    "sort_order": 1,
    "is_active": true,
    "metadata": null,
    "created_at": "2026-05-24T10:00:00.000000Z",
    "updated_at": "2026-05-24T10:00:00.000000Z"
  }
]
```

**Single section response (GET by key):**

```json
{
  "id": 1,
  "key": "hero",
  "title": "Fresh From Local Farmers",
  ...
}
```

**Input fields for `POST`/`PUT`:**

| Field | Type | Required |
|-------|------|----------|
| `key` | string | yes |
| `title` | string | yes |
| `subtitle` | string | no |
| `description` | string | no |
| `image_url` | string | no |
| `button_text` | string | no |
| `button_link` | string | no |
| `badge_text` | string | no |
| `sort_order` | integer | no |
| `is_active` | boolean | no |
| `metadata` | JSON | no |

> Note: Sections are unordered by default (sorted by `sort_order`). Only active sections are returned in public listings.

---

## Cart (session-based)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/cart` | Get current cart items |
| `GET` | `/api/cart/summary` | Get cart totals |
| `POST` | `/api/cart` | Add item to cart |
| `PUT` | `/api/cart/{id}` | Update item quantity |
| `DELETE` | `/api/cart/{id}` | Remove item from cart |
| `DELETE` | `/api/cart` | Clear entire cart |

**Cart item response:**

```json
{
  "data": [
    {
      "id": 1,
      "product_id": 1,
      "product_name": "Organic Bananas",
      "price": 2.99,
      "unit": "bunch",
      "unit_price": 0.3,
      "image_url": "http://localhost:8000/storage/media/products/abc123.jpg",
      "quantity": 2,
      "created_at": "2026-05-24T10:00:00.000000Z",
      "updated_at": "2026-05-24T10:00:00.000000Z"
    }
  ]
}
```

**Summary response:**

```json
{
  "items": [ ... ],
  "subtotal": 5.98,
  "delivery_fee": 3.99,
  "service_fee": 0.50,
  "total": 10.47,
  "count": 2
}
```

**Add to cart body:**

| Field | Type | Required |
|-------|------|----------|
| `product_id` | integer | yes |
| `product_name` | string | yes |
| `price` | numeric | yes |
| `quantity` | integer | no (default 1) |
| `unit` | string | no |
| `unit_price` | numeric | no |
| `image_url` | string | no |

---

## Basket (session-based, checkout step)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/basket` | Get basket |
| `POST` | `/api/basket` | Add item to basket |
| `PUT` | `/api/basket/{id}` | Update item quantity (0 = remove) |
| `DELETE` | `/api/basket/{id}` | Remove basket item |
| `DELETE` | `/api/basket` | Clear basket |

**Basket response:**

```json
{
  "items": [ ... ],
  "subtotal": 5.98,
  "delivery_fee": 3.99,
  "service_fee": 0.50,
  "total": 10.47,
  "vat_amount": 0.84,
  "item_count": 2
}
```

**Add to basket body:**

| Field | Type | Required |
|-------|------|----------|
| `product_id` | integer | yes |
| `product_name` | string | yes |
| `price` | numeric | yes |
| `quantity` | integer | no (default 1) |
| `unit` | string | no |
| `unit_price` | numeric | no |
| `image_url` | string | no |

---

## Checkout & Orders (session-based)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/checkout/payment-methods` | List active payment methods |
| `POST` | `/api/orders` | Create order |
| `GET` | `/api/orders/{id}` | Get order details |
| `POST` | `/api/orders/{id}/pay` | Process payment |

**Create order body:**

| Field | Type | Required |
|-------|------|----------|
| `items` | array | yes |
| `items[].product_id` | integer | yes |
| `items[].product_name` | string | yes |
| `items[].price` | numeric | yes |
| `items[].quantity` | integer | yes |
| `subtotal` | numeric | yes |
| `total` | numeric | yes |
| `vat_amount` | numeric | yes |

**Pay body:** `payment_method` (string)

**Order statuses:**

| Status | Description |
|--------|-------------|
| `pending` | Awaiting payment confirmation |
| `confirmed` | Payment received, order acknowledged |
| `processing` | Being packed / prepared |
| `shipped` | Handed to courier |
| `delivered` | Received by customer |
| `cancelled` | Order cancelled |

**Payment statuses:**

| Status | Description |
|--------|-------------|
| `pending` | No payment yet |
| `paid` | Payment successful |
| `failed` | Payment declined |
| `refunded` | Money returned to customer |

**Order response:**

```json
{
  "data": {
    "id": 1,
    "order_number": "BRN-ORD-001",
    "status": "pending",
    "payment_status": "pending",
    "payment_method": null,
    "subtotal": 8.48,
    "delivery_fee": 3.99,
    "service_fee": 0.50,
    "total": 12.97,
    "vat_amount": 0.68,
    "notes": "Awaiting payment confirmation.",
    "items": [
      {
        "product_id": 1,
        "product_name": "Organic Bananas",
        "price": 2.99,
        "quantity": 2,
        "subtotal": 5.98
      }
    ],
    "created_at": "2026-05-24T10:00:00.000000Z",
    "updated_at": "2026-05-24T10:00:00.000000Z"
  }
}
```

**Admin order management** at `/admin/orders` — browse, filter by status, update order/payment statuses, add notes.

---

## Special Pages

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/save-the-food` | Save The Food page (hero, stats, products) |
| `GET` | `/api/weekly-specials` | List active weekly specials |

---

## Media

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/media` | List all media |
| `POST` | `/api/media` | Upload an image file |
| `GET` | `/api/media/{id}` | Get media by ID |
| `DELETE` | `/api/media/{id}` | Delete media |

**Upload (multipart/form-data):**

| Field | Type | Required |
|-------|------|----------|
| `file` | file | yes |
| `type` | string | no (default: image) |
| `mediable_type` | string | no (e.g. `Modules\Product\Models\Product`) |
| `mediable_id` | integer | no |

**Upload response:**

```json
{
  "id": 1,
  "url": "http://localhost:8000/storage/media/products/abc123.jpg",
  "filename": "bananas.jpg",
  "mime_type": "image/jpeg",
  "size": 102400
}
```

---

## Authentication

The public storefront does **not** require authentication. Cart, basket, and orders are tied to the browser session automatically via cookies.

For admin (`/admin/*` routes), use session-based login at `POST /admin/login` with `email` and `password`.

**Sanctum (token) endpoint** (optional): `GET /api/user` with `Authorization: Bearer {token}`

---

## Frontend Integration Notes

1. **Session cookies** are set automatically by the browser when you call any session-based endpoint (cart, basket, orders). Ensure your frontend sends cookies (`credentials: "include"` or `withCredentials: true`).
2. **No API tokens** are needed for the public storefront.
3. All write operations on cart/basket/orders require an active session (first `GET` request establishes it).
4. All API responses are wrapped in a `data` envelope (Laravel JSON Resource standard).

**Image upload** — two approaches:

**A) Upload directly when creating/updating a product or category** — send `image` as a file field in a multipart/form-data request to `POST /api/products`, `PUT /api/products/{id}`, `POST /api/categories`, or `PUT /api/categories/{id}`. The backend uploads the file and sets `image_url` automatically.

**B) Upload separately via the media endpoint** — send a `POST /api/media` request with the file. Use the returned `url` as the `image_url` value when creating/updating a product or category.

**Example fetch (image upload with product):**

```js
const formData = new FormData();
formData.append("name", "Organic Apples");
formData.append("slug", "organic-apples");
formData.append("price", "3.99");
formData.append("image", fileInput.files[0]); // the file

await fetch("http://localhost:8000/api/products", {
  method: "POST",
  credentials: "include",
  body: formData, // no Content-Type header — browser sets it with boundary
});
```

**Example fetch (upload to media endpoint then reference):**

```js
// Step 1: upload file
const mediaForm = new FormData();
mediaForm.append("file", fileInput.files[0]);
const upload = await fetch("http://localhost:8000/api/media", {
  method: "POST",
  body: mediaForm,
});
const { url } = await upload.json();

// Step 2: use url in product
await fetch("http://localhost:8000/api/products", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "...", slug: "...", price: 9.99, image_url: url }),
});
```

**Supported image formats:** jpg, jpeg, png, gif, webp, svg — max 2MB.

---

## Database

The app uses **MySQL** by default. To switch to SQLite:

1. Set `DB_CONNECTION=sqlite` in `.env`
2. Remove `DB_DATABASE` from `.env` (it will default to `database/database.sqlite`)

**Artisan commands:**

| Command | Description |
|---------|-------------|
| `php artisan products:fetch-images` | Download picsum.photos images for all products |
| `php artisan products:fetch-images --force` | Re-download even if image exists |
| `php artisan db:migrate-sqlite-to-mysql` | Migrate all data from SQLite → MySQL (run while SQLite is active) |

---

## Tech Stack

- **Laravel 13** | **PHP ^8.3** | **MySQL** (SQLite fallback)
- Modular monolith via `nwidart/laravel-modules`
- Session driver: `database`
