---
feature: Operational notifications
slug: notifications
surface: admin
consumer: vue
version: 0.1.0
status: in-progress
brd_version: "Morvia Project Guide"
brd_ids: [ADM-047, PLT-004]
business_rules: []
decisions: [DEC-27, DEC-30, TD-12, TD-14]
schema_tables: [users, notifications, admin_notifications]
screens: []
related_docs: []
last_updated: 2026-11-12
---

# Operational notifications — React Dashboard Integration Guide

> Consumer: Vue. Admin base URL: `{HOST}/api/v1/admin`; shared inbox base: `{HOST}/api/v1`. [API conventions](../API_CONVENTIONS.md).

## Quick reference

| | |
|---|---|
| Version | 0.1.0 (2026-11-12); dashboard action: account for blockers before integration. |
| Status | In progress; base routes exist, ADM-047 and PLT-004 are incomplete. |
| Auth | Bearer JWT + active account. Admin routes also use RBAC; shared routes do not. |

| Method | Full path | Route name / permission | Purpose |
|---|---|---|---|
| GET | `/api/v1/admin/notifications` | `admin.notifications.index` | Paginate all database notification rows. |
| POST | `/api/v1/admin/notifications/send` | `admin.notifications.create` | Attempt manual send; currently broken. |
| GET | `/api/v1/notifications` | `shared.notifications.index` | Own inbox; currently broken. |
| GET | `/api/v1/notifications/{id}` | `shared.notifications.show` | Mark own notification read. |
| DELETE | `/api/v1/notifications/{id}` | `shared.notifications.destroy` | Delete own notification. |
| POST | `/api/v1/notifications/mark-all-read` | `shared.notifications.mark-all-read` | Mark own inbox read. |
| DELETE | `/api/v1/notifications/clear-all` | `shared.notifications.clear-all` | Delete own inbox. |

**Statuses:** read/unread; send target `broadcast`/`targeted` (§7). **Errors:** 401, 403, 404, 422, 500; no established feature error codes. **Open:** DEC-27 manual broadcast, DEC-30 channels. Numeric recipient IDs violate TD-14; see §13.

## 1. Business summary

ADM-047 asks administrators to follow operational notifications, while PLT-004 requires each user to have an in-app inbox. The admin index reads notification records across recipients; the shared routes operate on the current user's own inbox. The manual send route exists but is not usable in its current implementation.

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) |
|---|---|---|---|
| Operational notifications | All-recipient raw record list | Admin GET | `admin.notifications.index` → `notifications.index` |
| Send form | Manual dispatch | Admin POST | `admin.notifications.create` → `notifications.create` |
| Personal inbox | List, mark read, delete, mark all, clear | Five shared routes in Quick reference | No `rbac.auto`; authenticated active user only. |

Send `Accept: application/json` and `Authorization: Bearer {token}` on all routes; use `Content-Type: application/json` for the send body. `Accept-Language: en` or `ar` and `X-Correlation-ID` are optional. The RBAC resolver strips `admin`: `admin.notifications.index` resolves to `notifications.index` and `admin.notifications.create` to `notifications.create`. Admin RBAC is bypassed in local/testing. Shared route names identify routes, not RBAC permission keys. Do not show an enabled send form while DEC-27 and the implementation remain unresolved.

## 3. List / table

`GET /api/v1/admin/notifications` returns raw Laravel database notifications across all notifiable users, newest first, not just sent broadcasts. `page` selects the page; `per_page` is cast to integer, clamped to 1–100, default 15. No search, filters, sorting control or `paginate=false` is implemented. Each recipient's row is separate.

Real HTTP 200 response from a passing isolated SQLite test with middleware disabled and one deliberately inserted fixture (the UUID and numeric recipient ID are fixture values):

```json
{"status":"success","message":"Notifications retrieved successfully","data":{"current_page":1,"data":[{"id":"018f0000-0000-7000-8000-000000000001","type":"ExampleNotification","notifiable_type":"App\\Models\\User","notifiable_id":42,"data":{"message":"Example","payload":[]},"read_at":null,"created_at":"2026-01-01T00:00:00.000000Z","updated_at":"2026-01-01T00:00:00.000000Z"}],"first_page_url":"http://localhost/api/v1/admin/notifications?page=1","from":1,"last_page":1,"last_page_url":"http://localhost/api/v1/admin/notifications?page=1","links":[{"url":null,"label":"&laquo; Previous","page":null,"active":false},{"url":"http://localhost/api/v1/admin/notifications?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next &raquo;","page":null,"active":false}],"next_page_url":null,"path":"http://localhost/api/v1/admin/notifications","per_page":15,"prev_page_url":null,"to":1,"total":1}}
```

| Field | Type | Meaning |
|---|---|---|
| `status`, `message` | string | Envelope. |
| `data.data[].id` | UUID string | Notification ID. |
| `type` | string | Laravel notification class, not a `{value,label}` enum. |
| `notifiable_type`, `notifiable_id` | string, number | Internal polymorphic class and numeric database ID; TD-14 violation, not a public user identifier. |
| `data` | object, array or null | Raw decoded notification data; shape depends on producer. |
| `read_at` | ISO datetime or null | Read timestamp, null when unread. |
| `created_at`, `updated_at` | ISO datetime or null | Record timestamps. |
| `admin_notification_id` | number or null, if present in actual table | Internal nullable FK in module migration; raw model serialization exposes table columns. Do not use as a public ID. |
| `data.current_page`, `last_page`, `per_page`, `total` | number | Pagination counts. |
| `data.from`, `to` | number or null | Current range. |
| `data.first_page_url`, `last_page_url`, `path` | string | Pagination URLs and base path. |
| `data.prev_page_url`, `next_page_url` | string or null | Adjacent pages. |
| `data.links[]` | `{url: string|null, label: string, page: number|null, active: boolean}` | Pagination controls. |

## 4. Details view

There is no admin notification detail endpoint. `GET /api/v1/notifications/{id}` is a **write action** marking the current user's notification read; it does not return a detail object. See §6.2.

## 5. Forms (create / edit)

There is no CRUD create or edit form. The separate manual-send FormRequest is in §6.1.

## 6. Actions (non-CRUD)

### 6.1 Manual send

`POST /api/v1/admin/notifications/send` · route `admin.notifications.create`, resolved permission `notifications.create`.

| Field | Type | Required | FormRequest validation | Notes |
|---|---|---|---|---|
| `title` | string | yes | `required`, `string`, `max:200` | One string, not bilingual. |
| `body` | string | yes | `required`, `string`, `max:1000` | One string. |
| `target` | string | yes | `required`, `in:broadcast,targeted` | Literal strings; no enum object. |
| `user_ids` | array | if targeted | `required_if:target,targeted`, `array` | Internal IDs, contrary to TD-14; no explicit minimum/maximum or distinct rule. |
| `user_ids.*` | integer | each supplied item | `integer`, `exists:users,id` | Numeric internal IDs, not UUIDs. |

Broadcast selects all active users; targeted selects IDs without an active filter. The service intends HTTP 200 with `data.recipients_count` as a number, but constructs `GeneralNotification` with an array where its constructor requires a string. Consequently valid sends encounter a TypeError before a success response or dispatch; do not assume delivery. No real success response was available.

Real HTTP 422 from the same isolated test with empty JSON:

```json
{"status":"fail","message":"The Title field is required.","data":null}
```

### 6.2 Personal inbox actions

`GET /api/v1/notifications`: fixed 15 per page; `page` selects page. The intended `data` contains numeric `unread_count` and `notifications` resource collection with `links`/`meta` and a dynamic item key (`super_admin_notifications`, `supervisor_notifications`, or `client_notifications` according to the intended match). However, the service refers to removed `UserType::SUPER_ADMIN`, `SUPERVISOR` and `CLIENT` cases, so the current index fails before producing this response. No successful response was captured.

The intended item resource contains `id` UUID string; `title` localized string or null from `data.payload.title`; `body` string or null from `data.message`; `type` and `notify_id` from `data.payload` (nullable, producer-defined); `created_at` localized formatted string `Y M d, H:i A` rather than ISO; `is_read` boolean; `additional_data` from `data.payload.meta` (nullable, producer-defined). No explicit related-item URL is guaranteed. Treat payload fields as untrusted/unknown until a working response is verified.

`GET /api/v1/notifications/{id}` marks an owned UUID read and returns HTTP 200 with `data: null`; `DELETE` at that path deletes an owned record and returns the same data shape. `POST /api/v1/notifications/mark-all-read` and `DELETE /api/v1/notifications/clear-all` update or delete all of the current user's records respectively and return `data: null`. These actions accept no body. Missing or another user's ID returns 404; no ownership information is disclosed. No real action response was captured.

| HTTP | `error_code` | When | Dashboard behavior |
|---|---|---|---|
| 401 | Not established | Missing/invalid token | Reauthenticate. |
| 403 | Not established | Inactive user/role, or missing admin permission | Restrict action and show message. |
| 404 | None in shared service | Missing or unowned inbox ID | Refresh list; show message. |
| 422 | None | Invalid send request; only first validation message returned | Show message and correct input. |
| 500 | Not established | Send constructor mismatch, shared inbox enum mismatch, or unexpected failure | Show failure, do not claim send succeeded. |

## 7. Statuses and enums

The API returns literal request values, not `{value,label}` objects. UI labels below are presentation suggestions, not API values or server-provided labels.

| Enum | `value` | Label (en) | Badge color | Actions allowed |
|---|---|---|---|---|
| Send target | `broadcast` | Broadcast | N/A | Attempts all active users; currently fails. |
| Send target | `targeted` | Targeted | N/A | Attempts selected users; currently fails. |
| Read state | `is_read: false` / `read_at: null` | Unread | UI choice | Mark read or delete own item. |
| Read state | `is_read: true` / `read_at` present | Read | UI choice | Delete own item; no mark-unread endpoint. |

## Data model

| Table | Read / write | Notes |
|---|---|---|
| `notifications` | read, update, delete | Admin index is global; shared actions are current-user scoped. A successful producer may create rows. |
| `users` | read | Manual send recipient selection. |
| `admin_notifications` | none in these services | Module schema has this table and optional FK on notifications; send does not create a campaign record. |

## 8. TypeScript types

```ts
export interface Envelope<T> { status: string; message: string; data: T }
export interface RawNotification {
  id: string; type: string; notifiable_type: string; notifiable_id: number;
  admin_notification_id?: number | null; data: Record<string, unknown> | unknown[] | null;
  read_at: string | null; created_at: string | null; updated_at: string | null;
}
export interface PageLink { url: string | null; label: string; page: number | null; active: boolean }
export interface NotificationPage {
  current_page: number; data: RawNotification[]; first_page_url: string;
  from: number | null; last_page: number; last_page_url: string; links: PageLink[];
  next_page_url: string | null; path: string; per_page: number;
  prev_page_url: string | null; to: number | null; total: number;
}
export type SendNotification = { title: string; body: string } &
  ({ target: 'broadcast'; user_ids?: number[] } | { target: 'targeted'; user_ids: number[] });
export interface InboxItem {
  id: string; title: string | null; body: string | null; type: unknown;
  created_at: string; is_read: boolean; notify_id: unknown; additional_data: unknown;
}
```

`SendNotification` describes the broken existing contract, not a TD-14-compliant integration; wait for UUID support. Inbox types are source-derived, not verified against a successful response.

## 9. Business rules enforced by the backend

| Rule | What the backend does | What the admin sees |
|---|---|---|
| PLT-004 partial | Shared record mutations scope to the authenticated user; missing/unowned IDs return 404. | Cannot mutate someone else's inbox through shared routes. |
| Base targeting | Broadcast selects active users; targeted selects matching users regardless of active status. | Intended recipient count is selected users, not confirmed deliveries; send currently fails. |
| Validation | Checks fields in §6.1 before invoking send. | First invalid-field message, HTTP 422. |

## 10. Notifications triggered

| Event | Who is notified | Channel |
|---|---|---|
| Manual send | Intended selected active broadcast users or targeted users | Currently none: constructor mismatch prevents dispatch. GeneralNotification intends queued delivery; channel behavior is not established by a successful send. |
| Inbox read/delete | N/A | None. |

ADM-047 requires automatic trainer/course/enrollment/cancellation/event notifications; this guide does not establish that those producers or ADM-051 per-type toggles are implemented.

## 11. Integration checklist

- [ ] Gate admin list and send using their exact route names; shared inbox requires active authentication without admin RBAC.
- [ ] Handle raw global records separately from the user's own inbox; paginate with their different page sizes.
- [ ] Treat notification IDs as UUIDs; never rely on exposed internal recipient IDs as public identifiers.
- [ ] Keep send unavailable until DEC-27, UUID targeting and the constructor defect are resolved; validate §6.1 after the contract is fixed.
- [ ] Treat GET `{id}` as a read mutation; confirm before clearing/deleting; refresh counts after mutations.
- [ ] Handle 401/403/404/422/500 and first validation message; verify locale, RTL, date display and related-item navigation after fixes.

## 12. Acceptance scenarios

| AC ID | Scenario | Expected |
|---|---|---|
| N/A (ADM-047) | Admin lists notification records | Paginated global raw rows; isolated middleware-disabled test captured a row. |
| N/A (ADM-047) | Submit empty send form | 422 and first required-field message; captured in isolated test. |
| N/A (ADM-047) | Valid manual send | Currently fails at constructor; do not display delivery success. |
| N/A (PLT-004) | User opens inbox and marks own item read | Intended own list and scoped read; list currently blocked by missing enum cases. |
| N/A (PLT-004) | Read/delete another user's item | Shared service returns 404 by ownership lookup; not runtime-verified here. |
| N/A (ADM-047) | Operational event occurs | BRD expects automatic notification and per-type settings; coverage not established. |

## 13. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|---|---|---|
| DEC-27 | Manual broadcast authorization remains open. | Route exists but fails before send; existence is not a business decision. |
| DEC-30 | Event channels remain open. | No verified end-to-end push, SMS or email delivery; trainer decision email is required by PLT-004. |
| TD-14 | API IDs must be UUIDs. | Admin index exposes numeric `notifiable_id` and potentially `admin_notification_id`; targeted send validates numeric `users.id`. Notification row ID itself is UUID. |
| TD-12 | Bilingual title/body storage required. | Send form accepts one string each; shared resource reads title from payload and body from message, not the specified bilingual `data.title`/`data.body` contract. |
| PLT-004 | Inbox and related links | Shared list references missing UserType enum cases; resource has no guaranteed related URL and date is formatted, not ISO. |
| ADM-047 / ADM-051 | Automatic event catalogue and per-type controls required. | These endpoints do not establish the full event catalogue or per-type toggle enforcement. |
| Verification | Responses and permission enforcement | Only dashboard-independent admin list and validation captures used isolated SQLite with middleware disabled; no send success, shared inbox success, production RBAC, queue or external channel verified. |

## Version history

| Version | Date | Type | Summary | Dashboard action required |
|---|---|---|---|---|
| 0.1.0 | 2026-11-12 | Initial | Document base admin and shared routes and blockers. | Integrate only after resolving listed blockers. |

## Changelog

### 0.1.0 — 2026-11-12

- **Added:** Admin notification and shared inbox contracts, validation and known gaps.
- **Changed:** —
- **Fixed:** —
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** Admin index/send and five shared notification routes.
- **Dashboard action required:** Gate controls and resolve blockers before integration.
- **BRD / decisions:** ADM-047, PLT-004, DEC-27, DEC-30, TD-12, TD-14.
