# Admin Dashboard — New Features

## Overview

This document covers three new feature areas added to the admin dashboard API:
Packages, Package Subscriptions, and Teams. All routes are protected and require
`auth:api`, `admin`, and `active` middleware, with permission checks enforced per action.

---

## 1. Packages

### What a Package Is

A package is a curated offer that bundles a specific set of providers at a discounted
price. Clients can subscribe to a package and receive a fixed number of orders at a
reduced rate. The discount is split between two cost bearers: the provider and the admin.

### Data Model

| Field | Type | Description |
|---|---|---|
| `name` | translatable string | Package name — sent and returned per locale |
| `description` | translatable string (nullable) | Package description — sent and returned per locale |
| `price` | decimal | Subscription price paid by the client |
| `orders_count` | integer | Number of orders included in the package |
| `discount_percentage` | decimal | Total discount applied to each order |
| `provider_discount_percentage` | decimal | Portion of the discount borne by the provider |
| `admin_discount_percentage` | decimal | Portion of the discount borne by the platform |
| `is_active` | boolean | Whether the package is visible to clients |

### Translations

`name` and `description` are translatable. Send them in the request nested under each locale key:

```json
{
  "ar": { "name": "...", "description": "..." },
  "en": { "name": "...", "description": "..." }
}
```

The show response returns both a `name` / `description` field in the current request locale
and a full `translations` object containing all locales, so the frontend can populate edit
forms without a separate fetch.

### Discount Constraint

`provider_discount_percentage + admin_discount_percentage` must equal `discount_percentage`
exactly (tolerance: 0.01). This is enforced in both the request validation layer and the
service layer. Sending an invalid split returns a 422 validation error.

### Provider Selection

Providers are attached to a package using their `user_id` values. Only users with
`user_type = provider`, `account_status = accepted`, and `is_active = true` are accepted.

### API Endpoints

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/packages` | List all packages (paginated) |
| `POST` | `/admin/packages` | Create a new package |
| `GET` | `/admin/packages/{id}` | Get a single package |
| `PUT` | `/admin/packages/{id}` | Update a package |
| `DELETE` | `/admin/packages/{id}` | Soft-delete a package |
| `PATCH` | `/admin/packages/{id}/status` | Toggle active/inactive |

### Index — Filters and Sorting

| Parameter | Type | Description |
|---|---|---|
| `search` | string | Searches translated `name` across all locales |
| `is_active` | boolean | Filter by active status |
| `sort_by` | string | `price`, `orders_count`, `discount_percentage`, `created_at` |
| `sort_order` | string | `asc` or `desc` (default: `desc`) |
| `per_page` | integer | Results per page (default: 15) |

### Index — Response Shape

Each item in the list returns:

| Field | Description |
|---|---|
| `id` | Package ID |
| `name` | Name in current request locale |
| `description` | Description in current request locale |
| `price` | Package price |
| `orders_count` | Orders included |
| `discount_percentage` | Total discount |
| `provider_discount_percentage` | Provider share of discount |
| `admin_discount_percentage` | Admin share of discount |
| `providers_count` | Number of attached providers |
| `is_active` | Active status |
| `created_at` | Unix timestamp |
| `updated_at` | Unix timestamp |

### Show — Response Shape

Includes everything in the index response plus:

| Field | Description |
|---|---|
| `translations` | Object with all locales: `{ ar: { name, description }, en: { name, description } }` |
| `providers_data` | Array of attached providers: `id`, `full_name`, `avatar` |

### Store Request Fields

| Field | Required | Rules |
|---|---|---|
| `{locale}.name` | Yes | string, max 255 |
| `{locale}.description` | No | string |
| `price` | Yes | numeric, min 0 |
| `orders_count` | Yes | integer, min 1 |
| `discount_percentage` | Yes | numeric, 0–100 |
| `provider_discount_percentage` | Yes | numeric, 0–100 |
| `admin_discount_percentage` | Yes | numeric, 0–100 |
| `provider_ids` | Yes | array of user IDs (min 1) |
| `provider_ids.*` | Yes | integer, exists in users, must be an active accepted provider |
| `is_active` | No | boolean (default: true) |

### Update Request Fields

All fields are `sometimes` — only send what needs to change. The discount split validation
runs only when at least one discount field is present in the request.

---

## 2. Package Subscriptions

### What a Subscription Is

A subscription is created when a client purchases a package. The package's pricing,
discount, and order count are snapshotted at purchase time, so later changes to the
package do not retroactively affect active subscriptions.

### Subscription Lifecycle

| Status | Description |
|---|---|
| `pending` | Created but payment not yet completed |
| `active` | Payment confirmed, orders can be consumed |
| `failed` | Payment attempt failed |
| `cancelled` | Cancelled by admin or system |

### API Endpoints

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/packages/subscriptions` | List all subscriptions (paginated) |
| `GET` | `/admin/packages/subscriptions/{id}` | Get a single subscription |
| `POST` | `/admin/packages/subscriptions/{id}/cancel` | Cancel a subscription |

> The `/subscriptions` routes are declared before `/{id}` in the route file to prevent
> Laravel from matching the literal string "subscriptions" as a package ID.

### Index — Filters

| Parameter | Type | Description |
|---|---|---|
| `search` | string | Searches subscription number, user full name, phone, or email |
| `package_id` | integer | Filter by package |
| `user_id` | integer | Filter by subscribing client |
| `status` | string | Filter by lifecycle status |
| `per_page` | integer | Results per page (default: 15) |

### Index — Response Shape

| Field | Description |
|---|---|
| `id` | Subscription ID |
| `price` | Price at time of subscription |
| `orders_count` | Total orders included |
| `remaining_orders` | Orders not yet consumed |
| `status` | Current lifecycle status |
| `paid_at` | Unix timestamp of payment |
| `created_at` | Unix timestamp |
| `user_data` | `id`, `full_name`, `avatar` of the subscribing client |
| `package_data` | `id`, `name` of the package (current locale) |

### Show — Response Shape

Includes everything in the index response plus:

| Field | Description |
|---|---|
| `discount_percentage` | Total discount snapshotted at purchase |
| `provider_discount_percentage` | Provider share snapshotted at purchase |
| `admin_discount_percentage` | Admin share snapshotted at purchase |
| `payment_method` | Payment method used |
| `cancelled_at` | Unix timestamp of cancellation |
| `cancel_reason` | Text reason recorded on cancellation |
| `updated_at` | Unix timestamp |
| `package_data.description` | Package description (current locale) |

### Cancel — Request Body

| Field | Required | Description |
|---|---|---|
| `cancel_reason` | No | Reason text; defaults to "Cancelled by admin" |
| `refund_wallet` | No | If `true` and subscription was `active`, refunds full price to client wallet |

Attempting to cancel an already-cancelled subscription returns a 400 domain error.

---

## 3. Teams

### What a Team Is

A team groups multiple providers under a single entity that delivers a specific service.
Teams have their own schedule, pricing, avatar, and a designated leader. Clients book
teams as a unit.

### Data Model

| Field | Type | Description |
|---|---|---|
| `name` | translatable string | Team name |
| `brief` | translatable string | Short description |
| `service_id` | integer | The service this team provides |
| `price` | decimal | Team service price |
| `years_of_experience` | integer | Representative experience level |
| `is_active` | boolean | Whether the team is visible and bookable |
| `avatar` | media (image) | Team profile image |
| `avg_rating` | float | Aggregated rating |
| `reviews_count` | integer | Total review count |
| `completed_orders_count` | integer | Total completed orders |

### Provider Selection Convention

All endpoints that accept `provider_ids` or `leader_id` take **user IDs** (`users.id`),
not internal provider record IDs. The `team_provider` pivot table directly links to
these user IDs.

**Recommended UI flow when creating or editing a team:**

1. The admin selects a service (`service_id`) first.
2. Use the existing providers API (`GET /admin/providers?service_id={id}&per_page=0`) to
   get the full list of providers filtered by that service.
3. From the returned list the admin picks the team members (`provider_ids`) and designates
   one of them as the leader (`leader_id`).
4. Both `provider_ids` and `leader_id` are sent as `user_id` values.

The backend enforces service eligibility on every write, so only providers who actually
offer the selected service will pass validation.

**Validation enforced on every write:**
- Every submitted user ID must have a corresponding provider profile.
- `leader_id` must be one of the user IDs present in `provider_ids`.
- All providers must offer the team's `service_id` (verified via `provider_service` pivot).

### Schedules

Schedules define the team's working days and hours. When `schedules` is included on
create or update, it fully replaces the existing schedule (complete replacement, not merge).

| Field | Required | Description |
|---|---|---|
| `schedules.*.day_of_week` | Yes | `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday` |
| `schedules.*.start_time` | Yes | Format `H:i` (e.g. `09:00`) |
| `schedules.*.end_time` | Yes | Format `H:i`, must be after `start_time` |
| `schedules.*.is_active` | No | Boolean, defaults to `true` |

### API Endpoints

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/teams` | List all teams (paginated) |
| `POST` | `/admin/teams` | Create a new team |
| `GET` | `/admin/teams/{id}` | Get a single team |
| `PUT` | `/admin/teams/{id}` | Update a team |
| `DELETE` | `/admin/teams/{id}` | Soft-delete a team |
| `PATCH` | `/admin/teams/{team}/status` | Toggle active/inactive |
| `POST` | `/admin/teams/{team}/providers` | Add providers to a team |
| `DELETE` | `/admin/teams/{team}/providers` | Remove providers from a team |
| `PUT` | `/admin/teams/{team}/leader` | Reassign the team leader |

### Index — Filters and Sorting

| Parameter | Type | Description |
|---|---|---|
| `search` | string | Searches translated `name` |
| `service_id` | integer | Filter by service |
| `is_active` | boolean | Filter by active status |
| `sort_by` | string | `name`, `price`, `is_active`, `created_at` |
| `sort_order` | string | `asc` or `desc` |
| `per_page` | integer | Results per page (default: 15) |

### Index — Response Shape

| Field | Description |
|---|---|
| `id` | Team ID |
| `name` | Name in current locale |
| `brief` | Brief in current locale |
| `avatar` | Avatar URL |
| `price` | Team price |
| `years_of_experience` | Experience level |
| `avg_rating` | Average rating |
| `reviews_count` | Total reviews |
| `completed_orders_count` | Completed orders |
| `is_active` | Active status |
| `providers_count` | Number of members |
| `created_at` | Unix timestamp |
| `service_data` | `id`, `name` of the associated service |
| `leader_data` | `id`, `full_name`, `avatar` of the leader |

### Show — Response Shape

Includes everything in the index response plus:

| Field | Description |
|---|---|
| `translations` | `{ ar: { name, brief }, en: { name, brief } }` |
| `updated_at` | Unix timestamp |
| `providers_data` | Array of all members: `id`, `full_name`, `avatar`, `avg_rating`, `is_leader`, `joined_at` |
| `schedules_data` | Array of schedule slots: `day_of_week`, `start_time`, `end_time`, `is_active` |

### Store Request Fields

| Field | Required | Rules |
|---|---|---|
| `{locale}.name` | Yes | string, max 255 |
| `{locale}.brief` | Yes | string, max 2000 |
| `service_id` | Yes | integer, exists in services |
| `price` | Yes | numeric, min 0 |
| `years_of_experience` | Yes | integer, 0–100 |
| `avatar` | Yes | integer, exists in media |
| `is_active` | No | boolean |
| `provider_ids` | Yes | array of user IDs (min 1) |
| `provider_ids.*` | Yes | integer, exists in users, distinct |
| `leader_id` | Yes | integer, exists in users |
| `schedules` | Yes | array |
| `schedules.*.day_of_week` | Yes | see days above |
| `schedules.*.start_time` | Yes | `H:i` |
| `schedules.*.end_time` | Yes | `H:i`, must be after start_time |
| `schedules.*.is_active` | No | boolean |

### Update Request Fields

All fields are `sometimes`. If `provider_ids` is omitted, existing members are kept. If
`leader_id` is sent without `provider_ids`, only the leader changes, the member list is
untouched.

### Add Providers — Request Fields

`POST /admin/teams/{team}/providers`

| Field | Required | Rules |
|---|---|---|
| `provider_ids` | Yes | array of user IDs (min 1) |
| `provider_ids.*` | Yes | integer, exists in users, must offer team's service |
| `leader_id` | No | user ID — must be in new or existing team members |

Uses `syncWithoutDetaching` — existing members are not removed.

### Remove Providers — Request Fields

`DELETE /admin/teams/{team}/providers`

| Field | Required | Rules |
|---|---|---|
| `provider_ids` | Yes | array of provider IDs (min 1), must be current team members |

If the removed provider was the leader, the first remaining member is automatically
promoted to leader.

### Update Leader — Request Fields

`PUT /admin/teams/{team}/leader`

| Field | Required | Rules |
|---|---|---|
| `leader_id` | Yes | user ID, must already be a member of the team |

---

## Common Notes

### User ID vs Provider ID

Packages and Teams accept providers via `user_id` in all API requests. This maps
directly to the `users` table via the `user_id` column on the respective pivot tables.
This is consistent across create, update, and all sub-resource endpoints.

### Soft Deletes

Both `Package` and `Team` use soft deletes. Deleted records do not appear in index
listings. Subscription records reference packages with `withTrashed()` so historical
data is preserved even after a package is deleted.

### Translatable Fields

| Model | Translatable fields |
|---|---|
| Package | `name`, `description` |
| Team | `name`, `brief` |

Request format:
```json
{
  "ar": { "name": "...", "description": "..." },
  "en": { "name": "...", "description": "..." }
}
```

All show responses include both a current-locale shorthand and a full `translations`
object keyed by locale.

### Response Envelope

All endpoints return the standard API envelope:
```json
{
  "status": "success",
  "message": "...",
  "data": { ... }
}
```

Paginated responses nest the collection under `data.data` with standard Laravel
pagination metadata (`current_page`, `last_page`, `per_page`, `total`).
