# Certificates & Invoices — frontend guide

Both features return **values only — no PDF, no image, no rendered file**. The
backend gives you structured JSON for a certificate or an invoice; the
frontend owns the visual layout (screen view, print, export, share). This
doc covers the certificate viewing/verification enrichment and the new
invoices feature end to end, so it doubles as the integration guide for
both.

## Endpoints covered

| Endpoint | Auth | Purpose |
|---|---|---|
| `GET /api/v1/user/certificates` | Bearer (user) | The signed-in user's own certificates, paginated |
| `GET /api/v1/user/certificates/{certificate}` | Bearer (user) | One certificate, 404 if it isn't the caller's |
| `GET /api/v1/certificates/verify/{qr_token}` | **None** | Public QR-scan verification page data |
| `GET /api/v1/user/invoices` | Bearer (user) | The signed-in user's own invoices (payments), paginated |
| `GET /api/v1/user/invoices/{invoice}` | Bearer (user) | One invoice, 404 if it isn't the caller's |

`{certificate}`/`{invoice}` are the resource's `uuid` (route-bound), never
the internal database id.

---

## Certificates

A certificate exists for exactly one of a **course** or a **program**
enrollment (never both). Course certificates auto-issue when the course's
quiz/content rules are met (`App\Services\Education\CertificateService`);
program certificates issue manually by an admin or auto-issue on attendance
threshold (`App\Services\Shared\CertificateService`). Both write to the same
`certificates` table and render through the same resource.

### `GET /user/certificates` and `/user/certificates/{certificate}`

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "0199...uuid",
    "certificate_number": "CERT-2026-AB12CD34",
    "type": "course",
    "recipient": { "name": "Sara Ahmed" },
    "program": null,
    "course": { "id": "course-uuid", "name": "Advanced Baking" },
    "trainer": { "id": "trainer-uuid", "name": "Chef Omar" },
    "issued_at": "2026-09-20 14:03:00",
    "status": "valid",
    "qr_token": "8f3c1e9a...",
    "verify_url": "https://api.example.com/api/v1/certificates/verify/8f3c1e9a..."
  }
}
```

The index endpoint wraps this same object in Laravel's standard paginator
shape (`data.data` is the array of certificates, `data.links`/`data.meta`
alongside it).

| Field | Notes |
|---|---|
| `type` | `"course"` or `"program"` — tells you which of `course`/`program` below is non-null |
| `program` / `course` | Exactly one is non-null, matching `type` |
| `trainer` | The course's or program's trainer, whichever applies |
| `status` | `"valid"` or `"revoked"` |
| `qr_token` | The raw token, if you want to build your own QR payload/URL |
| `verify_url` | The full API URL that a QR code should point at — see below |

### `GET /certificates/verify/{qr_token}` — public verification page

This is the page a QR scan lands on. **No auth, no login prompt** — anyone
with the certificate or its QR code can hit this.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "trainee_name": "Sara Ahmed",
    "program_name": "Advanced Baking",
    "certificate_number": "CERT-2026-AB12CD34",
    "issued_at": "2026-09-20 14:03:00",
    "status": "valid"
  }
}
```

`program_name` holds the course name when it's a course certificate (BRD
calls this field "course or program name" — one merged field by design).
Deliberately minimal: **no phone, email, payment, or account data** ever
appears here, even though the full certificate resource above has more.
Unknown/invalid `qr_token` → `404`.

### Rendering guidance

- **QR code image**: generate it client-side from `verify_url` (any QR
  library — `qrcode.react`, `qr_flutter`, etc.). The backend does not render
  a QR image, only the token and the URL it should encode.
- **Certificate view / "download as PDF"**: build the certificate layout
  natively in the app using the fields above, and use the platform's own
  print-to-PDF / screenshot-share flow if a "download" action is needed. No
  backend PDF endpoint exists or is planned for this.

---

## Invoices

New feature. An invoice is just a `Payment` row rendered for display — it
covers **all three** payment kinds behind one shape: course/program
**enrollment**, book **order**, and **consultation booking**. There's no
separate "invoice" table; `invoice_number` is derived deterministically from
the payment's internal id (`INV-` + the id zero-padded to 6 digits), not
stored anywhere.

### `GET /user/invoices` and `/user/invoices/{invoice}`

Enrollment payment:

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "0199...uuid",
    "invoice_number": "INV-000042",
    "type": "enrollment",
    "status": "success",
    "payment_method": "card",
    "transaction_reference": "TXN-1A2B3C",
    "currency": "EGP",
    "amount": 199.0,
    "shipping_fee": 0.0,
    "total_amount": 199.0,
    "issued_at": "2026-09-20T10:00:00+00:00",
    "paid_at": "2026-09-20T10:00:05+00:00",
    "billed_to": { "name": "Sara Ahmed", "email": "sara@example.com", "phone": "+201000000000" },
    "items": [
      {
        "title": "Advanced Baking",
        "kind": "course",
        "quantity": 1,
        "unit_price": 199.0,
        "line_total": 199.0,
        "enrolled_at": "2026-09-20T10:00:05+00:00"
      }
    ]
  }
}
```

Order payment — `items` is one row per book:

```json
"items": [
  { "title": "Pastry Basics", "kind": "book", "quantity": 2, "unit_price": 50.0, "line_total": 100.0 }
]
```

Consultation booking payment — one item, consultant/trainer shown instead of a course:

```json
"items": [
  {
    "title": "Career Consultation",
    "kind": "consultation",
    "trainer": "Chef Omar",
    "quantity": 1,
    "unit_price": 150.0,
    "line_total": 150.0,
    "scheduled_at": "2026-10-01T12:00:00+00:00"
  }
]
```

| Field | Notes |
|---|---|
| `type` | `"enrollment"` \| `"order"` \| `"consultation_booking"` |
| `status` | `"success"` \| `"failed"` \| `"pending"` — invoices are **not** filtered to successful payments only; a failed/pending payment still returns an invoice with that status, so the UI should render it accordingly rather than assume every invoice is paid |
| `currency` | ISO-ish currency **code** (e.g. `"EGP"`), not a symbol |
| `amount` / `shipping_fee` / `total_amount` | Floats, always present (`shipping_fee` is `0.0` for non-order payments) |
| `items[].kind` | `"course"` \| `"program"` \| `"book"` \| `"consultation"` — use this to pick line-item icon/label, not `type` (which is the payment's, not the item's) |

### Rendering guidance

Same as certificates: no PDF/image from the backend. Build the invoice
layout from these values; the `items` array is already shaped as line items
(`title`, `quantity`, `unit_price`, `line_total`) so a generic line-item
table component can render all three payment kinds without per-type UI
branching, aside from the extra fields (`enrolled_at`, `scheduled_at`,
`trainer`) that only apply to some kinds.

---

## Files touched

**Certificates (enrichment only — feature already existed):**
- `app/Http/Resources/Client/Certificate/CertificateResource.php` — added `recipient`, `verify_url`
- `app/Services/Shared/CertificateService.php` — eager-loads `user` for the above

**Invoices (new):**
- `database/migrations/2026_09_24_000001_add_uuid_to_payments_table.php` — gives `payments` a public `uuid`
- `app/Models/Payment.php` — `HasUuid` trait
- `app/Services/Client/InvoiceService.php`
- `app/Http/Resources/Client/Invoice/InvoiceResource.php`
- `app/Http/Controllers/Api/V1/Client/InvoiceController.php`
- `routes/api/v1/user.php` — `user.invoices.index` / `user.invoices.show`

## Tests

`tests/Feature/InvoiceTest.php` (list/show, ownership isolation, order line
items) and the existing `tests/Feature/CertificateTest.php` — all passing.
Regenerate `docs/postman-collection.json` / `docs/api/user.md` via
`php artisan postman:generate && php artisan postman:markdown` if these
routes change again.
