# Service Provider Administration Web Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Extend the current server-session admin panel with provider moderation, verification review, protected documents, status history, and service-catalog management.

**Architecture:** Reuse `AdminSessionManager` and the existing admin shell. A declarative admin provider route registry and focused payload resolver protect the BFF; focused JS view/viewmodel chunks extend the current router without moving authorization authority out of `api_mycode`.

**Tech Stack:** PHP 8.3+, Laravel 13, Blade, vanilla JavaScript, Laravel Mix, PHPUnit 12.

---

### Task 1: Admin provider permissions, routes, and shell configuration

**Files:**
- Modify: `app/Domain/Admin/AdminPermission.php`
- Modify: `app/Presentation/ViewModels/AdminPanelConfigViewModel.php`
- Modify: `app/Presentation/ViewModels/AdminPanelConfigFactory.php`
- Modify: `app/Presentation/Http/Controllers/Admin/AdminPanelController.php`
- Modify: `resources/views/admin/panel.blade.php`
- Modify: `resources/views/admin/partials/app-shell.blade.php`
- Modify: `routes/web.php`
- Create: `tests/Feature/AdminServiceProviderPanelAccessTest.php`

- [ ] **Step 1: Write failing route and permission tests**

```php
public function test_provider_admin_shells_require_admin_session(): void
{
    $this->get('/admin/service-providers')->assertRedirect('/admin/login');
    $this->get('/admin/service-providers/11111111-1111-4111-8111-111111111111')
        ->assertRedirect('/admin/login');
    $this->get('/admin/service-catalogs')->assertRedirect('/admin/login');
}

public function test_permission_catalog_contains_provider_permissions(): void
{
    self::assertContains('view_service_providers', AdminPermission::all());
    self::assertContains('review_service_providers', AdminPermission::all());
    self::assertContains('verify_service_providers', AdminPermission::all());
    self::assertContains('suspend_service_providers', AdminPermission::all());
    self::assertContains('manage_service_provider_catalogs', AdminPermission::all());
}
```

- [ ] **Step 2: Run tests and confirm failure**

Run: `php artisan test tests/Feature/AdminServiceProviderPanelAccessTest.php`

Expected: FAIL because routes and permissions are absent.

- [ ] **Step 3: Add exact permission constants**

```php
public const VIEW_SERVICE_PROVIDERS = 'view_service_providers';
public const REVIEW_SERVICE_PROVIDERS = 'review_service_providers';
public const VERIFY_SERVICE_PROVIDERS = 'verify_service_providers';
public const SUSPEND_SERVICE_PROVIDERS = 'suspend_service_providers';
public const MANAGE_SERVICE_PROVIDER_CATALOGS = 'manage_service_provider_catalogs';
```

Add translated Spanish labels to `catalog()` without changing API authority.

- [ ] **Step 4: Add routes and ViewModel URLs**

Register `/admin/service-providers`,
`/admin/service-providers/{provider}`,
`/admin/service-provider-verifications/{verification}`, and
`/admin/service-catalogs` inside `admin.web`. Repeat through the existing
dedicated-domain closure.

Expose `adminServiceProvidersUrl` and `adminServiceCatalogsUrl` from the
ViewModel and as root data attributes. Add sidebar links with
`data-permission`.

- [ ] **Step 5: Run tests**

Run: `php artisan test tests/Feature/AdminServiceProviderPanelAccessTest.php tests/Feature/AdminPanelAccessTest.php`

Expected: PASS and existing panel routes remain unchanged.

- [ ] **Step 6: Commit**

```bash
git add app/Domain/Admin/AdminPermission.php app/Presentation resources/views/admin routes/web.php tests/Feature/AdminServiceProviderPanelAccessTest.php
git commit -m "feat: add provider administration shells"
```

Choose **Mantener** in the version hook.

### Task 2: Secure admin provider bridge and payloads

**Files:**
- Create: `app/Application/Bridge/AdminProviderRouteRegistry.php`
- Create: `app/Application/Bridge/AdminProviderPayloadResolver.php`
- Modify: `app/Presentation/Http/Controllers/Bridge/AdminBridgeController.php`
- Modify: `app/Application/Bridge/AdminBridgeResponseFactory.php`
- Create: `tests/Unit/Application/Bridge/AdminProviderRouteRegistryTest.php`
- Create: `tests/Unit/Application/Bridge/AdminProviderPayloadResolverTest.php`
- Create: `tests/Feature/AdminBridgeServiceProvidersTest.php`

- [ ] **Step 1: Write failing allowlist tests**

```php
public function test_provider_moderation_routes_are_explicit(): void
{
    $registry = new AdminProviderRouteRegistry();
    $providerId = '11111111-1111-4111-8111-111111111111';
    $verificationId = '22222222-2222-4222-8222-222222222222';

    self::assertTrue($registry->allows('GET', 'service-providers'));
    self::assertTrue($registry->allows('POST', "service-providers/{$providerId}/approve"));
    self::assertTrue($registry->allows('GET', "service-provider-verifications/{$verificationId}/document"));
    self::assertFalse($registry->allows('DELETE', "service-providers/{$providerId}"));
    self::assertFalse($registry->allows('POST', "service-providers/{$providerId}/arbitrary"));
}
```

Feature tests assert the admin bearer token comes only from the server session.

- [ ] **Step 2: Run tests**

Run: `php artisan test tests/Unit/Application/Bridge/AdminProviderRouteRegistryTest.php tests/Feature/AdminBridgeServiceProvidersTest.php`

Expected: FAIL.

- [ ] **Step 3: Implement route registry**

Allow only:

```php
private const UUID = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}';

private const ROUTES = [
    ['GET', '#^service-providers$#'],
    ['GET', '#^service-providers/' . self::UUID . '$#'],
    ['POST', '#^service-providers/' . self::UUID . '/(approve|reject|suspend|restore|verify)$#'],
    ['POST', '#^service-provider-verifications/' . self::UUID . '/review$#'],
    ['GET', '#^service-provider-verifications/' . self::UUID . '/document$#'],
    ['POST', '#^service-catalogs/(categories|services|specialties|features)$#'],
    ['PUT|DELETE', '#^service-catalogs/(categories|services|specialties|features)/' . self::UUID . '$#'],
    ['GET', '#^public/service-catalogs/(categories|services|specialties|features)$#'],
];
```

Provider administration routes map to `/api/v1/admin/{path}` upstream;
public catalog reads map to `/api/v1/public/service-catalogs/{catalog}`, and
existing users, stats, and error-log routes continue to map to `/admin/{path}`.

`AdminBridgeController` delegates provider paths to this registry and existing
user/error paths to their current rules. Do not add route-by-route `if`
statements.

- [ ] **Step 4: Implement exact validation**

Moderation payloads:

```php
'approve' => [],
'reject' => ['reason' => ['required', 'string', 'max:1000']],
'suspend' => ['reason' => ['required', 'string', 'max:1000']],
'restore' => [],
'verify' => [
    'verification_level' => ['required', 'string', Rule::in([
        'unverified',
        'email_verified',
        'phone_verified',
        'identity_verified',
        'business_verified',
        'documents_verified',
        'mycode_verified',
    ])],
    'reason' => ['nullable', 'string', 'max:1000'],
],
'verification_review' => [
    'status' => ['required', 'string', Rule::in(['approved', 'rejected'])],
    'rejection_reason' => ['nullable', 'required_if:status,rejected', 'string', 'max:1000'],
    'expires_at' => ['nullable', 'date', 'after:today'],
],
```

Catalog rules validate scope, name, slug, description, parent, data type,
filter flag, active flag, icon, and sort order.

- [ ] **Step 5: Support protected PDF/image documents**

Extend `AdminBridgeResponseFactory` to permit `application/pdf`, images, and
octet-stream, preserving only content type, disposition, and private cache
headers.

- [ ] **Step 6: Run tests**

Run: `php artisan test tests/Unit/Application/Bridge/AdminProviderRouteRegistryTest.php tests/Unit/Application/Bridge/AdminProviderPayloadResolverTest.php tests/Feature/AdminBridgeServiceProvidersTest.php`

Expected: PASS, including 404 for unknown paths and 422 for invalid reasons.

- [ ] **Step 7: Commit**

```bash
git add app/Application/Bridge app/Presentation/Http/Controllers/Bridge/AdminBridgeController.php tests
git commit -m "feat: secure provider moderation bridge"
```

Choose **Mantener**.

### Task 3: Provider list, filters, and permission-aware navigation

**Files:**
- Create: `resources/js/admin/18-provider-routes.js`
- Create: `resources/js/admin/19-provider-list-model.js`
- Create: `resources/js/admin/20-provider-list-view.js`
- Create: `resources/js/admin/21-provider-list-viewmodel.js`
- Create: `public/css/admin-panel/14-service-providers.css`
- Modify: `resources/js/admin/01-chunk.js`
- Modify: `resources/js/admin/02-routes.js`
- Modify: `resources/views/admin/partials/styles.blade.php`
- Modify: `webpack.mix.js`
- Create: `tests/js/admin-service-providers-list.test.js`
- Modify: `package.json`

- [ ] **Step 1: Write failing list tests**

```javascript
assert.deepStrictEqual(buildProviderAdminQuery({
    status: 'pending_review',
    verification_level: 'business_verified',
    provider_type: 'business',
    region: 'Región Metropolitana',
    search: 'veterinaria',
    page: 2,
}), {
    status: 'pending_review',
    verification_level: 'business_verified',
    provider_type: 'business',
    region: 'Región Metropolitana',
    search: 'veterinaria',
    page: 2,
});
```

Assert navigation is hidden without `view_service_providers` and catalog
navigation is hidden without `manage_service_provider_catalogs`.

- [ ] **Step 2: Run the JS test**

Run: `node tests/js/admin-service-providers-list.test.js`

Expected: FAIL because model/viewmodel functions are absent.

- [ ] **Step 3: Extend config, state, and router**

Add:

```javascript
serviceProvidersUrl: root.dataset.serviceProvidersUrl,
serviceCatalogsUrl: root.dataset.serviceCatalogsUrl,
```

The router recognizes list/detail/verification/catalog paths before its
fallback. `isAdminPath()` includes both new roots.

- [ ] **Step 4: Implement list model and viewmodel**

The model normalizes `data`, `links`, and `meta`. The viewmodel requests:

```javascript
adminFetch(`${config.apiBridgeUrl}/admin/service-providers?${params}`)
```

It preserves filters in the URL, handles pagination, cancels stale requests
with `AbortController`, and renders loading, empty, error, and populated states.

- [ ] **Step 5: Implement responsive list view**

Columns/cards show display name, provider type, status, verification, primary
category, region/commune, owner ID, and created date. Filters cover every API
filter. All controls have labels and status badges include text.

- [ ] **Step 6: Run tests and build**

Run:

```bash
node tests/js/admin-service-providers-list.test.js
npm run test:admin-dashboard
npm run test:admin-users-status
npm run production
```

Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add resources/js/admin public/css/admin-panel resources/views/admin/partials/styles.blade.php webpack.mix.js public/js/admin/app.js tests/js package.json package-lock.json
git commit -m "feat: list and filter service providers in admin"
```

Choose **Mantener**.

### Task 4: Provider detail, moderation, and status history

**Files:**
- Create: `resources/js/admin/22-provider-detail-model.js`
- Create: `resources/js/admin/23-provider-detail-view.js`
- Create: `resources/js/admin/24-provider-moderation-viewmodel.js`
- Create: `public/css/admin-panel/15-provider-detail.css`
- Modify: `webpack.mix.js`
- Create: `tests/js/admin-service-provider-detail.test.js`
- Create: `tests/Feature/AdminProviderModerationTest.php`

- [ ] **Step 1: Write failing moderation tests**

```javascript
assert.deepStrictEqual(availableProviderActions({
    status: 'pending_review',
    permissions: ['review_service_providers'],
}), ['approve', 'reject']);

assert.deepStrictEqual(availableProviderActions({
    status: 'published',
    permissions: ['suspend_service_providers', 'verify_service_providers'],
}), ['suspend', 'verify']);
```

Feature tests assert approve, reject, suspend, restore, and verify requests
forward exact paths/payloads.

- [ ] **Step 2: Run tests**

Run: `node tests/js/admin-service-provider-detail.test.js && php artisan test tests/Feature/AdminProviderModerationTest.php`

Expected: FAIL.

- [ ] **Step 3: Normalize and render provider details**

Sections show identity, owner ID, contacts, categories, branches, operations,
verifications, rejection/suspension reasons, timestamps, and status events.
Each status event shows action, previous/new state, actor, reason, and date.

- [ ] **Step 4: Implement moderation actions**

Actions use a confirmation dialog. Reject and suspend require a non-empty
reason. Verify requires `verification_level` and accepts an optional reason. After success,
reload the resource and announce the new state.

UI permission checks affect visibility only:

```javascript
const canReview = can('review_service_providers');
const canSuspend = can('suspend_service_providers');
const canVerify = can('verify_service_providers');
```

- [ ] **Step 5: Run tests and build**

Run: `node tests/js/admin-service-provider-detail.test.js && php artisan test tests/Feature/AdminProviderModerationTest.php && npm run production`

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add resources/js/admin public/css/admin-panel webpack.mix.js public/js/admin/app.js tests
git commit -m "feat: moderate service providers in admin"
```

Choose **Mantener**.

### Task 5: Verification review and protected documents

**Files:**
- Create: `resources/js/admin/25-provider-verification-view.js`
- Create: `resources/js/admin/26-provider-verification-viewmodel.js`
- Modify: `resources/js/admin/18-provider-routes.js`
- Modify: `webpack.mix.js`
- Create: `tests/js/admin-provider-verification.test.js`
- Create: `tests/Feature/AdminProviderVerificationTest.php`

- [ ] **Step 1: Write failing verification tests**

```javascript
assert.deepStrictEqual(buildVerificationReview({
    status: 'rejected',
    rejection_reason: 'Documento ilegible',
    expires_at: '',
}), {
    status: 'rejected',
    rejection_reason: 'Documento ilegible',
    expires_at: null,
});
```

Feature tests verify PDF response type/disposition and that requests without
admin session receive 401 before upstream calls.

- [ ] **Step 2: Run tests**

Run: `node tests/js/admin-provider-verification.test.js && php artisan test tests/Feature/AdminProviderVerificationTest.php`

Expected: FAIL.

- [ ] **Step 3: Implement verification screen**

Render verification type, current status, creation/review/expiry timestamps,
reviewer, rejection reason, and document action. Document links point only to
the same-origin admin bridge.

- [ ] **Step 4: Implement review workflow**

Approval allows optional expiration. Rejection requires reason. Successful
review reloads both verification and provider data. A 403 shows missing
permission; a 401 triggers current admin logout handling.

- [ ] **Step 5: Run tests and build**

Run: `node tests/js/admin-provider-verification.test.js && php artisan test tests/Feature/AdminProviderVerificationTest.php && npm run production`

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add resources/js/admin webpack.mix.js public/js/admin/app.js tests
git commit -m "feat: review provider verifications in admin"
```

Choose **Mantener**.

### Task 6: Service catalog CRUD

**Files:**
- Create: `resources/js/admin/27-service-catalog-model.js`
- Create: `resources/js/admin/28-service-catalog-view.js`
- Create: `resources/js/admin/29-service-catalog-viewmodel.js`
- Create: `public/css/admin-panel/16-service-catalogs.css`
- Modify: `webpack.mix.js`
- Create: `tests/js/admin-service-catalogs.test.js`
- Create: `tests/Feature/AdminServiceCatalogBridgeTest.php`

- [ ] **Step 1: Write failing catalog tests**

```javascript
assert.deepStrictEqual(buildCatalogPayload('features', {
    name: 'Estacionamiento',
    slug: 'estacionamiento',
    scope: 'both',
    description: '',
    data_type: 'boolean',
    is_filterable: true,
    is_active: true,
    icon: 'local_parking',
    sort_order: '20',
}), {
    name: 'Estacionamiento',
    slug: 'estacionamiento',
    scope: 'both',
    description: null,
    data_type: 'boolean',
    is_filterable: true,
    is_active: true,
    icon: 'local_parking',
    sort_order: 20,
});
```

Feature tests cover POST, PUT, DELETE for all four catalog types and reject
unknown catalog names.

- [ ] **Step 2: Run tests**

Run: `node tests/js/admin-service-catalogs.test.js && php artisan test tests/Feature/AdminServiceCatalogBridgeTest.php`

Expected: FAIL.

- [ ] **Step 3: Implement catalog model and view**

Tabs represent categories, services, specialties, and features. Rows show
name, slug, scope, parent/type, filter flag, active state, and order. The form
adapts fields by catalog:

- categories: optional parent;
- services and specialties: scope;
- features: data type and filter flag.

Each tab loads its initial collection from the allowlisted same-origin admin
bridge path `/api/bridge/admin/public/service-catalogs/{catalog}`.

- [ ] **Step 4: Implement create/update/delete**

All mutations require `manage_service_provider_catalogs` in UI and are still
authorized by the API. Delete uses an explicit confirmation containing the
catalog item name. API 409/422 messages remain visible without losing form
values.

- [ ] **Step 5: Run tests and build**

Run: `node tests/js/admin-service-catalogs.test.js && php artisan test tests/Feature/AdminServiceCatalogBridgeTest.php && npm run production`

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add resources/js/admin public/css/admin-panel webpack.mix.js public/js/admin/app.js tests
git commit -m "feat: manage service provider catalogs"
```

Choose **Mantener**.

### Task 7: Admin regression and browser acceptance

**Files:**
- Modify: `tests/Feature/ArchitectureComplianceTest.php`
- Create: `docs/service-provider-admin-web.md`

- [ ] **Step 1: Extend architecture and token-leak checks**

Assert every new admin JS file stays below 150 lines, the panel shell remains
below 80 lines, no inline JS/CSS enters `panel.blade.php`, and admin JSON
responses contain no bearer token.

- [ ] **Step 2: Run complete automated verification**

Run:

```bash
php artisan test
npm run test:admin-dashboard
npm run test:admin-users-status
node tests/js/admin-service-providers-list.test.js
node tests/js/admin-service-provider-detail.test.js
node tests/js/admin-provider-verification.test.js
node tests/js/admin-service-catalogs.test.js
npm run production
php artisan architecture:audit --strict
git diff --check
```

Expected: all commands PASS.

- [ ] **Step 3: Run browser acceptance**

Using admin users with representative permissions, verify:

1. list and all filters;
2. provider detail and history;
3. approve/reject;
4. suspend/restore;
5. verify/unverify;
6. verification document and review;
7. each catalog CRUD;
8. navigation/action hiding without permissions;
9. 403 behavior when API denies a forged request.

- [ ] **Step 4: Document operations**

`docs/service-provider-admin-web.md` records routes, required permissions,
bridge allowlist, document security, build steps, and smoke checks.

- [ ] **Step 5: Commit**

```bash
git add tests/Feature/ArchitectureComplianceTest.php docs/service-provider-admin-web.md
git commit -m "test: verify provider administration end to end"
```

Choose **Mantener**.
