# 13 — Master Data: Services

## Overview

CRUD for the **services catalogue** — the master list of vendor offering types (Catering, Photography, Decoration, etc.). Every vendor service binding, every event service line item, and every analytics pie slice references this list.

**Status:** Live

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| SVC-01 | Ops (perm 1) | See a paginated list of services with booking counts | I know which categories are active |
| SVC-02 | Ops | Add a new service with icon, description, placeholder text, status | Expand the platform |
| SVC-03 | Ops | Edit a service's name, description, status | Fix mistakes |
| SVC-04 | Ops | Toggle status 0/1 | Hide a service from both apps without deleting |

---

## Screens & Flows

```
┌────────────────────────────┐
│ /services    list.blade.php│
└────────────┬───────────────┘
             │ DataTable AJAX
             ▼
┌────────────────────────────┐
│ /ajaxServiceList           │
│ ServiceModel::serviceList()│
└────────────────────────────┘

Add Service ──▶ /add-services (GET)  → addService.blade.php
                /store-service (POST)  → ServiceModel insert
Edit Service ──▶ /service/{id} (GET)  → modal data JSON
                /service (POST)        → ServiceModel update
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/services` | GET | `ServiceController::services()` | List page |
| `/ajaxServiceList` | GET | `ServiceController::ajaxServiceList()` | DataTable JSON |
| `/service/{id}` | GET | `ServiceController::getService()` | Modal data |
| `/service` | POST | `ServiceController::updateService()` | Update |
| `/add-services` | GET | `ServiceController::addService()` | Add form |
| `/store-service` | POST | `ServiceController::storeService()` | Validated insert |

---

## Data Model

### `services`

```php
// App\Models\ServiceModel
protected $table = 'services';
public $timestamps = false;

{
  id:                       int,
  uuid:                     string|null,
  name:                     string,           // required, max 50
  icon:                     string,           // required, max 255 (filename / URL)
  description:              string,           // required, max 255
  placeholder_description:  string,           // required, max 255
  status:                   0|1,
  user:                     int|null,         // sometimes joined to users for user_type filter (legacy?)
}
```

> `services.user` column is joined in `serviceList()` SQL with `LEFT JOIN users ON services.user = users.id` to derive a `user_type` filter. Origin / purpose unclear — **needs verification**.

### List Computed Columns

```php
booking_count = (SELECT COUNT(id) FROM event_services WHERE service_id = services.id)
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Create requires name, icon, description, placeholder_description, status | All `required`, max-lengths as in model |
| Status must be 0 or 1 | `in:0,1` |
| Update has no validation | `updateService` just writes whatever arrives |
| Status as integer or "1"-string both work | `where('status', 1)` and `WHERE status = '1'` |
| `bookedServiceCounts` (used by Analytics pie) | Filters `status = '1'` |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/services` | session + permission 1 | — | HTML | Browser |
| GET | `/ajaxServiceList` | session (AJAX whitelisted) | `draw, start, length, user_type?` | DataTable JSON | Service list |
| GET | `/service/{id}` | session + permission 1 | path id | `{ id, name, description, status }` | Edit modal |
| POST | `/service` | session + permission 1 | `service_id, service_name, service_status, description` | redirect | Edit form |
| GET | `/add-services` | session + permission 1 | — | HTML | Browser |
| POST | `/store-service` | session + permission 1 | `name, icon, description, placeholder_description, status` | redirect | Add form |

---

## Upstream Impact

- **No upstream** — services are root master data.

---

## Downstream Impact

- **Feature 05 (Vendor Mgmt)** — service multi-select for vendor onboarding & "Add Service" modal.
- **Feature 06 (Event Oversight)** — event services list joins here.
- **Feature 07 (Coordinator)** — vendor-by-service filter on candidate list.
- **Feature 09 (RM Create Event)** — services dropdown.
- **Feature 11 (Analytics)** — pie chart per service.
- **Public Client app** — service catalogue listing.
- **Public Vendor app** — selectable services during onboarding.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Renaming `services.name` | Every visible label across both apps updates immediately | Medium | UI |
| Toggling status 0 | Vendors bound to this service still have `vendor_services` rows; coordinator candidate query still uses them; public app catalogue hides them | High | Data |
| Removing `placeholder_description` | Add form throws because it's a required field | High | Data |
| Adding a new required column without defaults | `storeService` insert breaks | High | Data |
| Changing `services.id` to UUID | Every join across `event_services.service_id`, `vendor_services.service_id`, `event_services_vendor_quote` reference, etc. | Critical | Data |
| Removing `services.user` legacy column | `serviceList()` SQL throws — even when `$type` param is null, the LEFT JOIN runs | High | Data |

---

## Known Issues

- **`serviceList()` SQL string-interpolates `$type`** without binding (line 16-20). The `$type` is read from `$request->user_type`. Not currently exploited (UI doesn't expose it) but a vector exists.
- **`updateService` has no validation** — anyone with permission 1 can blank-out a service or set invalid status.
- **`services.user` legacy column** is joined unconditionally. If removed, list breaks.
- **No icon validation** — `icon` accepts any string up to 255 chars. The frontend assumes it's a filename or URL.
- **No soft delete** — status toggle is the only way to "remove" a service.
