# Build Field Mapper & DLQ Dashboard (Full Stack)

| | |
|---|--|
| **Priority** | High |
| **Story Points** | 8 |
| **Assignee** | TBD |

## Context

Our multi-tenant sync architecture needs a user interface for admins to manage rejected synchronization events (**Dead Letter Queue**) and define schema mapping rules for custom NetSuite fields (`custentity_*`). Because the heavy event orchestrator pipeline (Epic 7) isn't built yet, you must develop the foundational backend models/controllers and the React UI against temporary simulated/mock array states.

## Description

Develop the full-stack **Remediation Layer** within SuiteX. Follow the strict **Blade + React** architectural pattern (Livewire is prohibited here).

- **Backend:** Create the infrastructure tables (`field_metadata` and `dead_letter_events`) and their respective REST API controllers extending `BaseApiController`.
- **Frontend:** Construct the **SyncFieldMapper** and **DlqDashboard** React components. Hook them up to your new API routes using the shared `apiClient.js` singleton.

## Deliverables

### 1. Backend Infrastructure

- **FieldMetadata** Model & Migration.
- **DeadLetterEvent** Model & Migration.
- **SyncMetadataController** (GET & POST). Note: Ensure regex validation on `field_id` payloads (`^cust[a-zA-Z0-9_]+$`).
- **SyncDlqController** (GET paginated, POST retry, POST dismiss).

### 2. Frontend Components (React/Blade)

- **SyncFieldMapper.jsx:** Mounted on `field-mapper.blade.php`. Form to submit mappings. Entity dropdown must be hardcoded to Project and Project Task for this MVP.
- **DlqDashboard.jsx:** Mounted on `dlq.blade.php`. Filtering table for failed events.
- **Dismiss logic:** Do not destroy the row on success; update state to `dismissed` and apply a disabled/gray visual class.
- **Retry logic:** Update state to `retried` and disable all action buttons for that row.
- **DlqInspectorModal.jsx:** Read-only modal showing the raw JSON payload with **highlight.js** (do **not** install react-json-view).

## Acceptance Criteria

- [ ] Backend models correctly enforce `$connection = 'mysql'` (bypassing tenant DBs) and filter by `TenantService::getCurrentTenantId()`.
- [ ] Tenant boundaries are preserved: `tenant_id` is never passed explicitly in **any** frontend network request body.
- [ ] `sync_error_queue` events successfully display in the DLQ UI and the JSON schema modal natively highlights syntax.

## Technical Notes & Implementation Guidance

To accelerate development, please adhere to the following base structures defined by the architecture team.

### 1. Backend: Core Models Architecture

Both tables live globally, not on the tenant DB. You must explicitly define the connection.

```php
// src/Domain/Sync/Models/FieldMetadata.php
namespace Domain\Sync\Models;
use Illuminate\Database\Eloquent\Model;
class FieldMetadata extends Model {
    protected $connection = 'mysql';
    public $incrementing = false; // Note: Ensure your migration handles this
    protected $fillable = ['tenant_id', 'record_type', 'field_id', 'field_type', 'is_synced', 'is_readonly', 'normalization_rule', 'conflict_policy'];
    protected $casts = ['is_synced' => 'boolean', 'is_readonly' => 'boolean', 'normalization_rule' => 'array'];
}
// src/Domain/Sync/Models/DeadLetterEvent.php
class DeadLetterEvent extends Model {
    protected $connection = 'mysql';
    protected $fillable = ['tenant_id', 'record_type', 'record_id', 'payload', 'error_reason', 'status'];
    protected $casts = ['payload' => 'array'];
}
```

### 2. Backend: API Routing

Add the following strictly to `routes/api.php` utilizing standard middleware.

```php
Route::middleware(['web', 'auth', 'throttle:200,1'])->prefix('v1/sync')->group(function () {
    Route::get('metadata/{recordType}', [\App\Http\Controllers\Api\v1\SyncMetadataController::class, 'index']);
    Route::post('metadata', [\App\Http\Controllers\Api\v1\SyncMetadataController::class, 'store']);

    Route::get('dlq', [\App\Http\Controllers\Api\v1\SyncDlqController::class, 'index']);
    Route::post('dlq/{id}/retry', [\App\Http\Controllers\Api\v1\SyncDlqController::class, 'retry']);
    Route::post('dlq/{id}/dismiss', [\App\Http\Controllers\Api\v1\SyncDlqController::class, 'dismiss']);
});
```

### 3. DLQ Mock Shape Reference

While the orchestrator is unbuilt, the `SyncDlqController@index` must return hardcoded mock data for the frontend to render the table. Use this exact shape for your mock objects:

```json
{
  "id": 9932,
  "tenant_id": 4515181,
  "status": "pending",
  "record_type": "project",
  "record_id": "12345",
  "error_reason": "Schema validation failed: 'isInactive' must be boolean, got string.",
  "payload": {
    "schemaVersion": "v1",
    "eventId": "018e5c2a-1234",
    "changes": { "title": "Acme Rollout", "isInactive": "false" }
  }
}
```
