# Provider Verification Evidence 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:** Permitir que un prestador envíe una o dos evidencias reales en PDF/JPG/PNG para identidad, empresa o documentos, manteniendo correo, teléfono y MyCode fuera del flujo autodeclarado.

**Architecture:** La API será la fuente de verdad y almacenará cada archivo en una entidad privada `ServiceProviderVerificationEvidence`. La web revalidará el contrato, reenviará múltiples archivos multipart y mostrará correo sólo como estado de cuenta. Los enums históricos se conservan, pero el endpoint del propietario usa una allowlist estricta.

**Tech Stack:** Laravel API, PostgreSQL, almacenamiento privado, Laravel HTTP Client multipart, Blade, JavaScript sin framework, PHPUnit y Node test runner.

---

### Task 1: Modelo y migración de evidencias

**Files:**
- Create: `api_mycode/database/migrations/2026_07_30_000001_create_service_provider_verification_evidences_table.php`
- Create: `api_mycode/app/Models/ServiceProviderVerificationEvidence.php`
- Modify: `api_mycode/app/Models/ServiceProviderVerification.php`
- Test: `api_mycode/tests/Feature/ServiceProviders/ServiceProviderVerificationEvidenceSchemaTest.php`

- [ ] **Step 1: Escribir la prueba de esquema en rojo**

La prueba debe exigir tabla, UUID, FK con cascada, ruta privada, MIME, nombre, tamaño e índices.

```php
Schema::hasColumns('service_provider_verification_evidences', [
    'id', 'service_provider_verification_id', 'file_path',
    'mime_type', 'original_name', 'size_bytes', 'created_at', 'updated_at',
]);
```

También debe crear una verificación histórica con `document_path`, ejecutar la migración/backfill y comprobar una sola evidencia asociada.

- [ ] **Step 2: Ejecutar en rojo**

Run: `php artisan test tests/Feature/ServiceProviders/ServiceProviderVerificationEvidenceSchemaTest.php`

Expected: FAIL porque no existe la tabla.

- [ ] **Step 3: Implementar migración y modelos**

Crear tabla con UUID primario, FK UUID en cascada e índice por verificación. Añadir:

```php
public function evidences(): HasMany
{
    return $this->hasMany(
        ServiceProviderVerificationEvidence::class,
        'service_provider_verification_id'
    );
}
```

El backfill debe insertar sólo cuando `document_path` no sea nulo y no exista ya una evidencia para la verificación.

- [ ] **Step 4: Ejecutar en verde**

Run: `php artisan test tests/Feature/ServiceProviders/ServiceProviderVerificationEvidenceSchemaTest.php`

Expected: PASS.

### Task 2: Contrato seguro del propietario

**Files:**
- Modify: `api_mycode/app/Enums/ServiceProviders/VerificationType.php`
- Modify: `api_mycode/app/Http/Requests/ServiceProviders/StoreProviderVerificationRequest.php`
- Modify: `api_mycode/app/Http/Controllers/Api/V1/ServiceProviders/OwnerProviderVerificationController.php`
- Modify: `api_mycode/app/Services/ServiceProviders/VerificationDocumentService.php`
- Test: `api_mycode/tests/Feature/ServiceProviders/OwnerProviderVerificationEvidenceApiTest.php`

- [ ] **Step 1: Escribir pruebas en rojo**

Cubrir:

```php
yield 'email' => ['email'];
yield 'phone' => ['phone'];
yield 'mycode' => ['mycode'];
```

Los tres deben responder 422. `identity`, `business` y `documents` deben aceptar uno o dos archivos bajo `evidence`. Cero o tres archivos, MIME distinto y archivo mayor a 10 MB deben responder 422.

- [ ] **Step 2: Ejecutar en rojo**

Run: `php artisan test tests/Feature/ServiceProviders/OwnerProviderVerificationEvidenceApiTest.php`

Expected: FAIL con el contrato actual singular.

- [ ] **Step 3: Implementar allowlist y validación**

En el enum:

```php
public static function ownerSubmittableValues(): array
{
    return [
        self::Identity->value,
        self::Business->value,
        self::Documents->value,
    ];
}
```

En el request:

```php
'verification_type' => ['required', Rule::in(VerificationType::ownerSubmittableValues())],
'evidence' => ['required', 'array', 'min:1', 'max:2'],
'evidence.*' => [
    'required', 'file', 'mimes:pdf,jpg,jpeg,png',
    'mimetypes:application/pdf,image/jpeg,image/png', 'max:10240',
],
'document' => ['prohibited'],
```

- [ ] **Step 4: Guardar todos los archivos de manera compensable**

`VerificationDocumentService::submit()` recibirá una lista de `UploadedFile`. Guardará cada archivo con UUID, creará la verificación y sus evidencias dentro de la mutación bloqueada y eliminará todos los archivos guardados si cualquier parte falla.

- [ ] **Step 5: Ejecutar en verde**

Run: `php artisan test tests/Feature/ServiceProviders/OwnerProviderVerificationEvidenceApiTest.php tests/Feature/ServiceProviders/ServiceProviderMediaTest.php`

Expected: PASS.

### Task 3: Recursos y descargas administrativas

**Files:**
- Create: `api_mycode/app/Http/Resources/ServiceProviders/ServiceVerificationEvidenceResource.php`
- Modify: `api_mycode/app/Http/Resources/ServiceProviders/ServiceVerificationResource.php`
- Modify: `api_mycode/app/Http/Resources/ServiceProviders/AdminServiceVerificationResource.php`
- Modify: `api_mycode/app/Http/Controllers/Api/V1/ServiceProviders/AdminProviderVerificationController.php`
- Modify: `api_mycode/routes/api/v1/service_providers.php`
- Test: `api_mycode/tests/Feature/ServiceProviders/AdminServiceProviderVerificationEvidenceApiTest.php`

- [ ] **Step 1: Escribir pruebas en rojo**

Exigir metadatos sin `file_path`, dos UUID de evidencia, descarga autorizada `private, no-store`, 403 sin permiso, 404 si la evidencia pertenece a otra verificación y compatibilidad del endpoint singular histórico.

- [ ] **Step 2: Ejecutar en rojo**

Run: `php artisan test tests/Feature/ServiceProviders/AdminServiceProviderVerificationEvidenceApiTest.php`

Expected: FAIL por falta de relación y ruta.

- [ ] **Step 3: Implementar recursos y ruta**

Agregar:

```php
Route::get(
    '/service-provider-verifications/{verification}/evidences/{evidence}',
    [AdminProviderVerificationController::class, 'evidence']
);
```

La acción debe autorizar la verificación, comprobar pertenencia exacta, descargar desde el disco privado y aplicar `Cache-Control: private, no-store`.

- [ ] **Step 4: Ejecutar en verde**

Run: `php artisan test tests/Feature/ServiceProviders/AdminServiceProviderVerificationEvidenceApiTest.php`

Expected: PASS.

### Task 4: Multipart múltiple en el puente web

**Files:**
- Create: `web_mycode/app/Infrastructure/Http/MultipartFile.php`
- Modify: `web_mycode/app/Infrastructure/Http/MultipartUpload.php`
- Modify: `web_mycode/app/Infrastructure/Http/MultipartRequestFactory.php`
- Modify: `web_mycode/app/Application/Bridge/ProviderUploadPayloadResolver.php`
- Test: `web_mycode/tests/Feature/ProviderBridgeVerificationContractTest.php`
- Test: `web_mycode/tests/Feature/ProviderBridgeUploadTest.php`

- [ ] **Step 1: Escribir pruebas en rojo**

Exigir que el puente rechace tipos prohibidos y reenvíe exactamente `evidence[0]` y `evidence[1]` con sus MIME detectados, sin aceptar `document`.

- [ ] **Step 2: Ejecutar en rojo**

Run: `php artisan test tests/Feature/ProviderBridgeVerificationContractTest.php tests/Feature/ProviderBridgeUploadTest.php`

Expected: FAIL.

- [ ] **Step 3: Implementar colección multipart**

`MultipartUpload` expondrá `files(): array` de `MultipartFile`. `MultipartRequestFactory` abrirá todos los streams, encadenará un `attach()` por archivo y cerrará cada stream en `finally`.

- [ ] **Step 4: Actualizar resolver**

Validar:

```php
'verification_type' => ['required', Rule::in(['identity', 'business', 'documents'])],
'evidence' => ['required', 'array', 'min:1', 'max:2'],
'evidence.*' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'],
'document' => ['prohibited'],
```

- [ ] **Step 5: Ejecutar en verde**

Run: `php artisan test tests/Feature/ProviderBridgeVerificationContractTest.php tests/Feature/ProviderBridgeUploadTest.php`

Expected: PASS.

### Task 5: Portal bilingüe

**Files:**
- Modify: `web_mycode/resources/views/provider/partials/verifications.blade.php`
- Modify: `web_mycode/resources/js/provider/19-verifications-contract.js`
- Modify: `web_mycode/resources/js/provider/19-verifications-viewmodel.js`
- Modify: `web_mycode/resources/js/provider/19-verifications-view.js`
- Modify: `web_mycode/resources/js/provider/02-session.js`
- Modify: `web_mycode/resources/lang/es/provider_portal.php`
- Modify: `web_mycode/resources/lang/en/provider_portal.php`
- Test: `web_mycode/tests/js/provider-review.test.js`
- Test: `web_mycode/tests/Feature/ProviderPortalReviewTest.php`

- [ ] **Step 1: Escribir pruebas en rojo**

Verificar exactamente tres opciones, input `multiple` llamado `evidence[]`, estado de correo de sólo lectura, uno o dos archivos válidos, rechazo de cero/tres y ausencia de teléfono/MyCode.

- [ ] **Step 2: Ejecutar en rojo**

Run: `node --test tests/js/provider-review.test.js && php artisan test tests/Feature/ProviderPortalReviewTest.php`

Expected: FAIL.

- [ ] **Step 3: Implementar formulario y contrato**

El formulario usará:

```blade
<input name="evidence[]" type="file" multiple
    accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png">
```

`providerVerificationInput()` usará `formData.getAll('evidence[]')`. El constructor de `FormData` agregará cada archivo con `body.append('evidence[]', file)`.

- [ ] **Step 4: Mostrar correo derivado**

Agregar un estado de sólo lectura y actualizarlo desde `providerRefreshSession()` usando exclusivamente `payload.user.email_verified_at`.

- [ ] **Step 5: Ejecutar en verde**

Run: `npm run test:provider && php artisan test tests/Feature/ProviderPortalReviewTest.php`

Expected: PASS.

### Task 6: Administración y regresión completa

**Files:**
- Modify: `web_mycode/resources/js/admin/25-provider-verification-view.js`
- Modify: `web_mycode/resources/js/admin/26-provider-verification-viewmodel.js`
- Modify: `web_mycode/app/Application/Bridge/AdminProviderRouteRegistry.php`
- Test: `web_mycode/tests/js/admin-provider-verification.test.js`
- Test: `web_mycode/tests/Feature/AdminBridgeServiceProviderVerificationTest.php`

- [ ] **Step 1: Probar listado y descargas múltiples en rojo**

Exigir dos botones de evidencia, URLs UUID exactas, ausencia de rutas privadas y descarga protegida por sesión administrativa.

- [ ] **Step 2: Implementar vista y allowlist**

Renderizar una acción por evidencia y registrar únicamente la ruta administrativa explícita:

```text
service-provider-verifications/{verification}/evidences/{evidence}
```

- [ ] **Step 3: Ejecutar pruebas focalizadas**

Run: `npm run test:admin && php artisan test tests/Feature/AdminBridgeServiceProviderVerificationTest.php`

Expected: PASS.

- [ ] **Step 4: Ejecutar verificación completa**

API:

```bash
composer validate --no-check-publish
php artisan test
```

Web:

```bash
composer validate --no-check-publish
php artisan architecture:audit --strict
php artisan test
npm run test:admin
npm run test:provider
npm run production
```

Expected: cero fallas.
