# 05 — Vendor Management

## Overview

CRUD on **vendors** (the rows in shared `users` table where `user_type = 'vendor'`). Includes the only mutating surface of consequence in VisoAdmin: a **multi-table delete cascade** that purges 14+ tables in one transaction.

**Status:** Live

> Vendor onboarding from the public app produces these rows too — but VisoAdmin is the place where ops manually create vendors during sales, edit them, bind them to services + cities, and permanently delete them.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| VEN-01 | Ops | See a list of vendors filtered by service & city | I can find a vendor for a specific quote follow-up |
| VEN-02 | Ops | Open a vendor detail page with profile, status, events count, and services | I can audit them |
| VEN-03 | Ops | Add a service (with multi-city location binding) to a vendor | I can extend their reach manually |
| VEN-04 | Ops | Edit name / email / phone / company / display_name / alt_number / status | I can correct vendor data |
| VEN-05 | Ops | Create a vendor account on their behalf | I can onboard them without the app |
| VEN-06 | Ops | Permanently delete a vendor with all their chat / quote / discount / feedback history | I can scrub bad actors completely |

---

## Screens & Flows

```
┌──────────────────────────┐
│ /user/vendorlist         │
│ vendorlist.blade.php     │
│ Filters: service, city,  │
│   mobile                 │
└────────────┬─────────────┘
             │ DataTable AJAX
             ▼
┌──────────────────────────┐
│ /user/ajaxUserList       │
│  ?user_type=vendor       │
│  + service, location     │
└────────────┬─────────────┘
             │  Click "View"
             ▼
┌──────────────────────────────┐    ┌─────────────────────────┐
│ /user/vendorDetails/{id}     │───▶│ /user/ajaxVendorDetails │
│ vendordetails.blade.php      │    │ basic + counts + svc[]  │
│                              │    └─────────────────────────┘
│  [Add Service modal] ────────┼──▶ POST /user/add-vendor-service
│  [Edit form] ────────────────┼──▶ POST /user/update-vendor/{id}
└──────────────────────────────┘

ADD VENDOR
┌─────────────────────────┐    POST /user/check-vendor      ┌────────────────┐
│ /user/add-vendor        │───────────────────────────────▶ │ phone exists?  │
│ addVendor.blade.php     │                                 └───────┬────────┘
└─────────────────────────┘                                         │ no
                            POST /user/create-vendor                ▼
                            ──────────────────────────▶ INSERT INTO users
                                                       (user_type='vendor', ...)

DELETE VENDOR ─── GET /user/deleteVendor/{id} ────▶ 14-statement transaction
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/user/vendorlist` | GET | `UsersController::vendorlist()` | List page (with service + city filter dropdowns) |
| `/user/vendorListAjax` | GET | `UsersController::vendorListAjax()` | Vendor dropdown source (id + name + email) |
| `/user/ajaxUserList` | GET | `UsersController::ajaxUserList()` | DataTable JSON (shared with client list) |
| `/user/vendorDetails/{id}` | GET | `UsersController::vendorDetails()` | Detail page |
| `/user/ajaxVendorDetails` | POST | `UsersController::ajaxVendorDetails()` | Detail JSON (basic + events count + vendor services + master service & city list) |
| `/user/add-vendor` | GET | `UsersController::addVendorForm()` | Add form |
| `/user/check-vendor` | POST | `UsersController::checkVendor()` | Phone-exists check |
| `/user/create-vendor` | POST | `UsersController::createVendor()` | INSERT vendor row |
| `/user/add-vendor-service` | POST | `VendorServiceController::addService()` | Add service + cities binding |
| `/user/update-vendor/{id}` | POST | `UsersController::updateVendor()` | Update vendor fields |
| `/user/deleteVendor/{id}` | GET | `UsersController::deleteVendor()` | Delete cascade |

---

## Data Model

### `users` (filtered to vendors)

```php
// App\Models\VendorUser → table 'users'
protected $fillable = [
  'id', 'name', 'uuid', 'phone',
  'user_type', 'email', 'password',
  'alternet_number', 'company_name',
  'display_name', 'status',
];

{
  id, uuid, name, display_name,
  phone,                   // login key
  email_id,
  alternet_number,         // sic — "alternate" mis-spelled in schema
  company_name,
  user_type:    'vendor',
  password:     bcrypt,
  status:       0 | 1,
  last_login_from: 'app'|'web'|null,
  address,
  created_at,
}
```

### `vendor_services`

```php
// App\Models\VendorServices → table 'vendor_services'
{
  id, uuid,
  vendor_id:    int,       // FK users.id
  service_id:   int,       // FK services.id
  description:  string,
  created_at, updated_at,
}
```

### `vendor_services_locations` (many-to-many)

```php
// App\Models\VendorServicesLocations
protected $fillable = ['vendor_services_id', 'location_id'];

{
  id,
  vendor_services_id: int,  // FK vendor_services.id
  location_id:        int,  // FK cities.id
}
```

### Detail Page Computed

```php
// from ajaxVendorDetails
basic_details: {
  status, date_of_registration, alt_number, company_name, email_id, last_login
}
about_events: {
  total_events:    COUNT(events WHERE saved_vendor_id = id),
  ongoing_events:  COUNT(events WHERE saved_vendor_id = id AND event_date >= today),
  complete_events: COUNT(events WHERE saved_vendor_id = id AND event_date < today),
  loss_events:     "0"     // hardcoded
}
vendor_service[]:  joined vendor_services + services + cities  (GROUP_CONCAT locations)
master_service_list[]: all active services (for the "Add Service" modal)
master_cities_list[]:  all active cities   (for the "Add Service" modal)
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Phone-uniqueness check | `checkVendor` queries `users WHERE phone=? AND user_type='vendor'` |
| Default password | `bcrypt($phone)` |
| UUID | Server-set `Str::uuid()` (and Webpatser UUID for vendor_services) |
| Add Service requires service_name + description + cities[] | `Validator::make(...)` in `VendorServiceController` |
| Each selected city becomes one `vendor_services_locations` row | Bulk insert via `create()` |
| Detail page can only be viewed by id (not uuid) | `vendorDetails/{id}` |
| Delete is hard-delete with cascade | See "Delete Cascade" below |

### Delete Cascade

`UsersController::deleteVendor()` runs (inside `DB::beginTransaction`):

```
1.  SELECT GROUP_CONCAT(id) FROM vendor_services WHERE vendor_id = $id
2.  DELETE FROM vendor_services            WHERE id IN(...)
3.  DELETE FROM vendor_services_locations  WHERE vendor_services_id IN(...)
4.  SELECT GROUP_CONCAT(id) FROM conversation WHERE vendor_id = $id  (limit 1!)
5.  SELECT GROUP_CONCAT(id) FROM chat_service WHERE conversation_id IN(...)
6.  DELETE FROM chat            WHERE chat_service_id IN(...)
7.  DELETE FROM chat_service    WHERE id IN(...)
8.  DELETE FROM conversation    WHERE vendor_id = $id
9.  DELETE FROM user_reports    WHERE vendor_id = $id
10. DELETE FROM event_services_vendor_quote WHERE vendor_id = $id
11. DELETE FROM event_vendor_discount        WHERE vendor_id = $id
12. DELETE FROM favorite_vendors            WHERE vendors_id = $id
13. DELETE FROM vendor_feedbacks            WHERE vendor_id = $id
14. DELETE FROM fcm_tokens                  WHERE user_id   = $id
15. DELETE FROM conversation                WHERE vendor_id = $id  (duplicate of 8)
16. DELETE FROM notification_setting        WHERE vendor_id = $id
17. DELETE FROM call_verification           WHERE vendor_id = $id
18. DELETE FROM users                       WHERE id        = $id
COMMIT (rollback on Exception)
```

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/user/vendorlist` | session + permission 5 | — | HTML | Browser |
| GET | `/user/vendorListAjax` | session (AJAX whitelisted) | — | `[{id, name, email_id}, ...]` | Notification setting dropdown |
| GET | `/user/ajaxUserList` | session (AJAX whitelisted) | `draw, start, length, user_type=vendor, mobile?, service?, location?` | DataTable JSON | Vendor list page |
| GET | `/user/vendorDetails/{id}` | session + permission 5 | path id, `?start, ?length` | HTML | Browser |
| POST | `/user/ajaxVendorDetails` | session (AJAX whitelisted) | `user=<id>` | `{ basic_details, about_events, vendor_service, master_service_list, master_cities_list }` | Detail page JS |
| GET | `/user/add-vendor` | session + permission 5 | — | HTML | Browser |
| POST | `/user/check-vendor` | session (AJAX whitelisted) | `phone` | `{ userExists, phone? }` | Add form |
| POST | `/user/create-vendor` | session (AJAX whitelisted) | `phone, user_type=vendor, name, email_id, company_name, alternet_number` | `{ status, msg, body: { user_id } }` | Add form |
| POST | `/user/add-vendor-service` | session + permission 5 | `service_name, vendor_id, description, cities[]` | redirect back | Detail page modal |
| POST | `/user/update-vendor/{id}` | session + permission 5 | `name, email_id, alternet_number, company_name, display_name, status` | JSON (also sets flash) | Detail page form |
| GET | `/user/deleteVendor/{id}` | session + permission 5 | path id | redirect to vendor list | Vendor list "Delete" link |

---

## Upstream Impact

- **Shared `users` table** — written by both OotboAPI vendor onboarding and VisoAdmin admin-create.
- **`services` master data** (Feature 13) — populates the "Add Service" dropdown.
- **`cities` master data** (Feature 12) — populates the city multi-select.
- **`vendor_services_locations`** — depends on cities existing first.

---

## Downstream Impact

- **Feature 06 (Event Oversight)** — vendor name appears via `saved_vendor_id` join.
- **Feature 07 (Coordinator Console)** — vendor dropdown per service/city excludes vendors who already quoted.
- **Feature 17 (Notification Settings)** — `notification_setting.vendor_id` references vendor row.
- **Feature 11 (Analytics)** — vendor counts in KPI cards.
- **Public Client app** — discovers vendors via these rows; sees their services + city coverage.
- **Public Vendor app** — vendor logs in with this row.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Any of the 14 tables the delete cascade touches | Cascade SQL silently fails or leaves orphans | Critical | Data |
| `users.user_type` enum | All vendor filters break | Critical | Data |
| `users.saved_vendor_id` semantics | "About Events" counts wrong | High | Data |
| Renaming `alternet_number` → `alternate_number` | `create-vendor`, `update-vendor`, detail JSON | High | Data |
| `vendor_services.uuid` source | `VendorServiceController` uses Webpatser UUID; switching to `Str::uuid()` is fine, switching off UUIDs entirely breaks the column | Medium | Data |
| Adding a NEW related table for vendors (e.g. `vendor_certifications`) | Delete cascade leaves orphans there — must update cascade | High | Data |
| `vendor_services_locations.vendors_id` typo in `favorite_vendors` (currently `vendors_id`) | Hard-coded in the cascade — schema fix could break delete | Medium | Data |

---

## Known Issues

- **The `conversation` table is deleted twice** (steps 8 and 15) in the cascade. Harmless but wasteful.
- **`favorite_vendors` uses `vendors_id`** (plural-s typo). Cascade hardcoded to it; fixing the typo without updating cascade breaks delete.
- **`loss_events` is hardcoded to "0"** in `ajaxVendorDetails` — never computed.
- **`vendorListAjax` ignores vendor `status`** — returns inactive vendors too. Notification routing dropdowns may include disabled vendors.
- **Vendor self-service from the public app** can never produce the same `vendor_services_locations` shape because OotboAPI may use a different binding mechanism. **Verify with the OotboAPI maintainer** whether VisoAdmin's `vendor_services_locations` writes are interoperable.
- **Detail page status badge** uses `status($vendorBasicDetails->status)` which expects `0`/`1` — but the update form posts any value the user enters. Edge case: text values render as "Inactive".
