# Orange Wine — API Part 12: Staff, Roles, Approvals, Reports and Notifications

Version 1.0 — 28 Sep 2026 · 21 endpoints · Base URL `/api/v1` · Read with Part 00 (conventions, error catalog, permissions).

## 1. Before you start

- Base URL `/api/v1` (webhooks: `/webhooks/...`). JSON, `snake_case` keys, prefixed string IDs, ISO 8601 UTC timestamps.
- Success: `{ data, request_id, correlation_id }`. Error: `{ error: { code, message, details, retryable } }`. Gated feature: `data.enabled = false`, `status = "pending_configuration"`.
- Auth via HttpOnly cookies (`access_token`, `refresh_token`); every non-GET browser request sends `X-XSRF-TOKEN`.
- Money is always `{ amount_minor, currency }`. Percentages are basis points.
- Commands marked Idempotency = Required need an `Idempotency-Key` header (UUID v4).
- Status never changes through PATCH — use the named command endpoints.
- Full conventions, error catalog, permission catalog and open items: Part 00.

### Error codes used in this part

| Code | HTTP | Meaning |
|---|---|---|
| `FORBIDDEN` | 403 | Authenticated, but missing permission, location scope, manager approval, or a valid webhook signature. |
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

### Permissions used in this part

| Permission | Grants | Endpoints |
|---|---|---|
| `notifications.manage` | Resend notifications (PROPOSED) | 1 |
| `reports.view` | Reports | 1 |
| `staff.manage` | Staff, roles, assignments, staff sessions | 14 |

### Endpoints requiring Idempotency-Key

| Method | Path | Title |
|---|---|---|
| POST | `/dashboard/staff` | Create staff |
| POST | `/dashboard/staff/{staffId}/disable` | Disable staff |
| POST | `/dashboard/manager-approvals` | Issue manager approval |

### PROPOSED and gated endpoints in this part

| Status | Method | Path |
|---|---|---|
| CONFIGURATION GATE | POST | `/dashboard/staff/{staffId}/mfa/enroll` |

## 2. Staff, Roles, Approvals, Reports and Notifications

D-05, D-06. Store Manager role: exactly one location. Disabling staff revokes all sessions.

| # | Method | Path | Title | Status |
|---|---|---|---|---|
| 2.1 | GET | `/dashboard/staff` | Staff list | CONFIRMED |
| 2.2 | POST | `/dashboard/staff` | Create staff | CONFIRMED |
| 2.3 | GET | `/dashboard/staff/{staffId}` | Staff detail | CONFIRMED |
| 2.4 | PATCH | `/dashboard/staff/{staffId}` | Update staff | CONFIRMED |
| 2.5 | POST | `/dashboard/staff/{staffId}/disable` | Disable staff | CONFIRMED |
| 2.6 | GET | `/dashboard/roles` | Roles | CONFIRMED |
| 2.7 | POST | `/dashboard/roles` | Create role | CONFIRMED |
| 2.8 | GET | `/dashboard/roles/{roleId}` | Role detail | CONFIRMED |
| 2.9 | PATCH | `/dashboard/roles/{roleId}` | Update role | CONFIRMED |
| 2.10 | GET | `/dashboard/permissions` | Permission catalog | CONFIRMED |
| 2.11 | GET | `/dashboard/staff/{staffId}/location-assignments` | Location assignments | CONFIRMED |
| 2.12 | POST | `/dashboard/staff/{staffId}/location-assignments` | Assign location | CONFIRMED |
| 2.13 | DELETE | `/dashboard/staff/{staffId}/location-assignments/{locationId}` | Remove location | CONFIRMED |
| 2.14 | POST | `/dashboard/staff/{staffId}/mfa/enroll` | Enroll MFA | CONFIGURATION GATE |
| 2.15 | GET | `/dashboard/staff/{staffId}/sessions` | Staff sessions | CONFIRMED |
| 2.16 | POST | `/dashboard/staff/{staffId}/sessions/{sessionId}/revoke` | Revoke staff session | CONFIRMED |
| 2.17 | POST | `/dashboard/manager-approvals` | Issue manager approval | CONFIRMED |
| 2.18 | GET | `/dashboard/reports/{report}` | Report | CONFIRMED |
| 2.19 | GET | `/dashboard/notifications` | Notifications log | CONFIRMED |
| 2.20 | GET | `/dashboard/notifications/{notificationId}/deliveries` | Delivery attempts | CONFIRMED |
| 2.21 | POST | `/dashboard/notifications/{notificationId}/resend` | Resend notification | CONFIRMED |

### 2.1 `GET /dashboard/staff`

**Staff list**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/staff` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| location_id | string · optional |  |
| status | enum · optional | `active` \| `disabled`. |
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/staff?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "staff_02",
        "first_name": "Jane",
        "last_name": "Doe",
        "email": "jane@orangewine.example",
        "status": "active",
        "roles": [
          {
            "id": "role_cashier",
            "name": "cashier"
          }
        ],
        "location_ids": [
          "loc_albany"
        ],
        "mfa_enrolled": false,
        "last_login_at": "2026-09-17T13:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].status | enum | `active` \| `disabled`. |

---

### 2.2 `POST /dashboard/staff`

**Create staff**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/staff` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/staff HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "first_name": "Jane",
  "last_name": "Doe",
  "email": "jane@orangewine.example",
  "role_ids": [
    "role_cashier"
  ],
  "location_ids": [
    "loc_albany"
  ]
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| first_name / last_name / email | string · required | Email unique among staff. |
| role_ids[] | string[] · required |  |
| location_ids[] | string[] · required | Store Manager: exactly one. |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "staff_02",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@orangewine.example",
    "status": "active",
    "roles": [
      {
        "id": "role_cashier",
        "name": "cashier"
      }
    ],
    "location_ids": [
      "loc_albany"
    ],
    "mfa_enrolled": false,
    "last_login_at": "2026-09-17T13:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

An invitation email lets the staff member set a password (PROPOSED).

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | A Store Manager role must be constrained to exactly one location. |

---

### 2.3 `GET /dashboard/staff/{staffId}`

**Staff detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/staff/{staffId}` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
GET /api/v1/dashboard/staff/{staffId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "staff_02",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@orangewine.example",
    "status": "active",
    "roles": [
      {
        "id": "role_cashier",
        "name": "cashier"
      }
    ],
    "location_ids": [
      "loc_albany"
    ],
    "mfa_enrolled": false,
    "last_login_at": "2026-09-17T13:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.4 `PATCH /dashboard/staff/{staffId}`

**Update staff**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/staff/{staffId}` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/staff/{staffId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "role_ids": [
    "role_store_manager"
  ]
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| first_name / last_name / role_ids | optional | Status changes via /disable only. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "staff_02",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@orangewine.example",
    "status": "active",
    "roles": [
      {
        "id": "role_cashier",
        "name": "cashier"
      }
    ],
    "location_ids": [
      "loc_albany"
    ],
    "mfa_enrolled": false,
    "last_login_at": "2026-09-17T13:00:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Request failed validation. `details.fields` maps field → messages. |

---

### 2.5 `POST /dashboard/staff/{staffId}/disable`

**Disable staff**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/staff/{staffId}/disable` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
POST /api/v1/dashboard/staff/{staffId}/disable HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "reason": "Left the company"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| reason | string · required |  |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "staff_02",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@orangewine.example",
    "status": "disabled",
    "roles": [
      {
        "id": "role_cashier",
        "name": "cashier"
      }
    ],
    "location_ids": [
      "loc_albany"
    ],
    "mfa_enrolled": false,
    "last_login_at": "2026-09-17T13:00:00Z",
    "sessions_revoked": 2
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.6 `GET /dashboard/roles`

**Roles**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/roles` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/roles HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "id": "role_cashier",
        "name": "cashier",
        "permissions": [
          "pos.sell",
          "register.open",
          "register.close"
        ],
        "single_location": false
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].single_location | boolean | `true` for Store Manager. |

---

### 2.7 `POST /dashboard/roles`

**Create role**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/roles` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/roles HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "name": "shift_lead",
  "permissions": [
    "pos.sell",
    "orders.fulfill"
  ],
  "single_location": false
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| name | string · required |  |
| permissions[] | string[] · required | From GET /dashboard/permissions. |
| single_location | boolean · optional |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "id": "role_shift_lead",
    "name": "shift_lead"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.8 `GET /dashboard/roles/{roleId}`

**Role detail**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/roles/{roleId}` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| roleId | string |  |

**Request example**

```http
GET /api/v1/dashboard/roles/{roleId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "role_cashier",
    "name": "cashier",
    "permissions": [
      "pos.sell"
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.9 `PATCH /dashboard/roles/{roleId}`

**Update role**

| Property | Value |
|---|---|
| Endpoint | `PATCH /api/v1/dashboard/roles/{roleId}` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| roleId | string |  |

**Request example**

```http
PATCH /api/v1/dashboard/roles/{roleId} HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "permissions": [
    "pos.sell",
    "inventory.view"
  ]
}
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "id": "role_cashier"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.10 `GET /dashboard/permissions`

**Permission catalog**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/permissions` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Request example**

```http
GET /api/v1/dashboard/permissions HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "code": "pos.sell",
        "description": "Sell at POS"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].code | string | See Appendix B. |

---

### 2.11 `GET /dashboard/staff/{staffId}/location-assignments`

**Location assignments**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/staff/{staffId}/location-assignments` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
GET /api/v1/dashboard/staff/{staffId}/location-assignments HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "location_id": "loc_albany",
        "name": "Orange Wine Albany"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.12 `POST /dashboard/staff/{staffId}/location-assignments`

**Assign location**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/staff/{staffId}/location-assignments` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
POST /api/v1/dashboard/staff/{staffId}/location-assignments HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "location_id": "loc_saratoga"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| location_id | string · required |  |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "items": [
      {
        "location_id": "loc_albany"
      },
      {
        "location_id": "loc_saratoga"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Errors**

| Code | HTTP | When |
|---|---|---|
| `VALIDATION_ERROR` | 422 | Store Manager already has a location |

---

### 2.13 `DELETE /dashboard/staff/{staffId}/location-assignments/{locationId}`

**Remove location**

| Property | Value |
|---|---|
| Endpoint | `DELETE /api/v1/dashboard/staff/{staffId}/location-assignments/{locationId}` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |
| locationId | string |  |

**Request example**

```http
DELETE /api/v1/dashboard/staff/{staffId}/location-assignments/{locationId} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "location_id": "loc_albany"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.14 `POST /dashboard/staff/{staffId}/mfa/enroll`

**Enroll MFA**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/staff/{staffId}/mfa/enroll` |
| Auth | Staff (self) |
| Status | CONFIGURATION GATE |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
POST /api/v1/dashboard/staff/{staffId}/mfa/enroll HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "factor": "totp",
    "otpauth_uri": "otpauth://totp/OrangeWine:jane?secret=...",
    "confirmed": false
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.15 `GET /dashboard/staff/{staffId}/sessions`

**Staff sessions**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/staff/{staffId}/sessions` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |

**Request example**

```http
GET /api/v1/dashboard/staff/{staffId}/sessions HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "session_id": "sess_601",
        "last_active_at": "2026-09-17T16:00:00Z",
        "user_agent_summary": "POS Chrome"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.16 `POST /dashboard/staff/{staffId}/sessions/{sessionId}/revoke`

**Revoke staff session**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/staff/{staffId}/sessions/{sessionId}/revoke` |
| Auth | Staff `staff.manage` |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| staffId | string |  |
| sessionId | string |  |

**Request example**

```http
POST /api/v1/dashboard/staff/{staffId}/sessions/{sessionId}/revoke HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "session_id": "sess_601",
    "revoked": true
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.17 `POST /dashboard/manager-approvals`

**Issue manager approval**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/manager-approvals` |
| Purpose | Produces a short-lived, single-use approval bound to one action and resource. |
| Auth | Manager (credentials re-entered) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Required |
| Rate limited | No |

**Request example**

```http
POST /api/v1/dashboard/manager-approvals HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>
Idempotency-Key: 6f1c2a4e-8d3b-4f6a-9c1e-2b7d5e8f0a13

{
  "action": "refund",
  "resource_type": "order",
  "resource_id": "ord_5001",
  "reason": "Damaged bottle refund",
  "manager_credentials": {
    "email": "manager@orangewine.example",
    "password": "••••••"
  }
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| action | enum · required | `refund` \| `price_override` \| `void` \| `inventory_adjustment` \| `gift_card_adjust` \| `gift_card_deactivate` \| `store_credit_adjust` \| `register_reconcile` \| `transfer_approve` \| `count_post`. |
| resource_type / resource_id | string · required | Approval only valid for this resource. |
| reason | string · required |  |
| manager_credentials | object · required on shared terminals | Re-authentication (PIN option PENDING). |

**Response — created (201)**

```http
HTTP/1.1 201 Created

{
  "data": {
    "approval_id": "approval_1001",
    "status": "approved",
    "approved_action": "refund",
    "approved_by": "staff_09",
    "expires_at": "2026-09-17T19:05:00Z"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| expires_at | ISO 8601 | Short-lived; consumed on use. |

**Errors**

| Code | HTTP | When |
|---|---|---|
| `FORBIDDEN` | 403 | Approver lacks manager permission for this location |
| `VALIDATION_ERROR` | 422 | Bad credentials |

---

### 2.18 `GET /dashboard/reports/{report}`

**Report**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/reports/{report}` |
| Auth | Staff `reports.view` |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| report | enum | `sales` \| `tenders` \| `tax` \| `margin` \| `inventory` \| `shrinkage` \| `fulfillment` \| `registers` \| `returns` \| `cancellations` \| `pre-arrivals` \| `rapid-ship-sla` \| `promotions`. |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| location_id | string · optional |  |
| from / to | YYYY-MM-DD · required | Store-local dates. |
| group_by | enum · optional | `day` \| `week` \| `month` \| `register` \| `cashier` \| `product` \| `category` \| `channel`. |

**Request example**

```http
GET /api/v1/dashboard/reports/{report} HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "report": "sales",
    "location_id": "loc_albany",
    "timezone": "America/New_York",
    "period": {
      "from": "2026-09-01",
      "to": "2026-09-22"
    },
    "metrics": {
      "gross_sales": {
        "amount_minor": 1250000,
        "currency": "USD"
      },
      "discounts": {
        "amount_minor": 42000,
        "currency": "USD"
      },
      "tax": {
        "amount_minor": 96000,
        "currency": "USD"
      },
      "net_sales": {
        "amount_minor": 1208000,
        "currency": "USD"
      }
    },
    "rows": []
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| timezone | IANA | Date boundaries use store time. |

---

### 2.19 `GET /dashboard/notifications`

**Notifications log**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/notifications` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Query parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| order_id | string · optional |  |
| type | string · optional |  |
| page | integer · optional · default 1 | 1-based page number. |
| per_page | integer · optional · default 24 · max 100 | Items per page. Above 100 → VALIDATION_ERROR (D-26, reject-vs-clamp still open). |
| sort | string · optional | Field name, prefix `-` for descending, e.g. `-created_at`. |

**Request example**

```http
GET /api/v1/dashboard/notifications?page=1&per_page=24 HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "notification_id": "ntf_1",
        "type": "order.received",
        "channel": "email",
        "recipient_masked": "c***@example.com",
        "status": "delivered",
        "created_at": "2026-09-17T16:00:10Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 24,
      "total": 1,
      "last_page": 1
    }
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

**Response keys**

| Key | Type / allowed values | Description |
|---|---|---|
| items[].status | enum | `queued` \| `sent` \| `delivered` \| `failed` \| `suppressed`. |

---

### 2.20 `GET /dashboard/notifications/{notificationId}/deliveries`

**Delivery attempts**

| Property | Value |
|---|---|
| Endpoint | `GET /api/v1/dashboard/notifications/{notificationId}/deliveries` |
| Auth | Staff |
| Status | CONFIRMED |
| CSRF header | Not required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| notificationId | string |  |

**Request example**

```http
GET /api/v1/dashboard/notifications/{notificationId}/deliveries HTTP/1.1
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
```

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "items": [
      {
        "delivery_id": "dlv_1",
        "status": "delivered",
        "provider_message_id": "pm_1",
        "attempted_at": "2026-09-17T16:00:12Z"
      }
    ]
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---

### 2.21 `POST /dashboard/notifications/{notificationId}/resend`

**Resend notification**

| Property | Value |
|---|---|
| Endpoint | `POST /api/v1/dashboard/notifications/{notificationId}/resend` |
| Auth | Staff `notifications.manage` (PROPOSED permission) |
| Status | CONFIRMED |
| CSRF header | Required |
| Idempotency-Key | Not used |
| Rate limited | No |

**Path parameters**

| Key | Type / allowed values | Description |
|---|---|---|
| notificationId | string |  |

**Request example**

```http
POST /api/v1/dashboard/notifications/{notificationId}/resend HTTP/1.1
Content-Type: application/json
Cookie: access_token=<jwt>; refresh_token=<opaque>; XSRF-TOKEN=<token>
X-XSRF-TOKEN: <token from XSRF-TOKEN cookie>

{
  "channel": "email"
}
```

**Request keys**

| Key | Type / allowed values | Description |
|---|---|---|
| channel | enum · required | `email` \| `sms`. |

**Response — success (200)**

```http
HTTP/1.1 200 OK

{
  "data": {
    "notification_id": "ntf_1",
    "delivery_id": "dlv_2",
    "status": "queued"
  },
  "request_id": "req_01J9Z8",
  "correlation_id": "cor_01J9Z8"
}
```

---
