# PRD: Helpdesk Ticket Importer — Bug Fixes
**Date:** 2026-04-30  
**Source:** `helpdesk-ticket-importer-failed.csv` (42 failing QA test cases)  
**Module:** Helpdesk → Ticket Importer (`HelpdeskTicketImportController`)

---

## Failing Test Case Index

| Group | Test IDs | Count | Theme |
|-------|----------|-------|-------|
| G1 | HD-TI-002 | 1 | Breadcrumb navigation |
| G2 | HD-TI-004 | 1 | Save & Continue disabled state |
| G3 | HD-TI-010, 013, 017, 021, 025, 028 | 6 | Field Validation Rules modal content |
| G4 | HD-TI-011, 012, 014–016, 018–020, 022–027, 029–030 | 17 | Import coercion & validation logic |
| G5 | HD-TI-031–039 | 9 | Assign To + user-attribution fields |
| G6 | HD-TI-040–047 | 8 | Date / time format handling |
| G7 | HD-TI-050 | 1 | Import history page navigation |
| **Total** | | **43** | |

---

## G1 — Breadcrumb Navigation (HD-TI-002)

### Root Cause
`resources/views/admin/layouts/layout.blade.php` line 39 has the breadcrumb include globally commented out:
```blade
{{-- @include('admin.components.breadcrumb') --}}
```
The controller correctly sets `$this->_data['breadcrumb']` for all ticket-importer views, but it is never rendered.

### Fix
Uncomment the breadcrumb include in `layout.blade.php` line 39:
```blade
@include('admin.components.breadcrumb')
```

If uncommenting globally breaks other pages (due to those pages not setting `$breadcrumb`), guard it:
```blade
@if(isset($breadcrumb) && !empty($breadcrumb))
    @include('admin.components.breadcrumb')
@endif
```

**Files:** `resources/views/admin/layouts/layout.blade.php`

---

## G2 — Save & Continue Button Disabled State (HD-TI-004)

### Root Cause
The `disabled` HTML attribute is correctly present on `#saveContinueBtn` in the Blade template. However, the `.green-btn-inner` CSS class likely does not define visual styling for the `:disabled` state, so the button appears identical to an enabled button despite being functionally disabled.

### Fix
Add a CSS rule in the ticket-importer page or in the global stylesheet:
```css
.green-btn-inner:disabled,
.green-btn-inner[disabled] {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}
```

Add this inside the `<style>` block in `@push('page_script')` of `ticket-importer.blade.php`.

**Files:** `resources/views/admin/helpdesk/ticket-importer.blade.php`

---

## G3 — Field Validation Rules Modal Content (HD-TI-010, 013, 017, 021, 025, 028)

All six sub-issues are in the modal table inside `ticket-importer.blade.php`.

### G3-A — Ticket Type (HD-TI-010)
**Current:** "Exact name of an active Ticket Type configured for your company (case-insensitive). Unmatched values are skipped with a warning; the ticket is still imported."

**Required:** Must also state the name can be exported from the Helpdesk Ticket Types page.

**Fix:** Update the Accepted Values cell:
```
Exact name of an active Ticket Type configured for your company (case-insensitive).
Ticket Type names can be exported from the Helpdesk Ticket Types settings page.
Unmatched values are skipped with a warning; the ticket is still imported without a type.
```

### G3-B — Ticket Outcome (HD-TI-013)
**Current:** "Exact name of an Outcome configured for your company (case-insensitive). Unmatched values are skipped with a warning; the ticket is still imported."

**Required:** Must state the name can be exported from the Helpdesk Ticket Outcomes page.

**Fix:** Update the Accepted Values cell:
```
Exact name of a Ticket Outcome configured for your company (case-insensitive).
Outcome names can be exported from the Helpdesk Ticket Outcomes settings page.
Unmatched values are skipped with a warning; the ticket is still imported without an outcome.
```

### G3-C — Status (HD-TI-017)
**Current modal lists:** `New, Open, In Progress, Additional, Resolved, Closed`

**Required:** Remove `Closed` — it is NOT an accepted import value (HD-TI-019 explicitly requires it to be rejected). The allowed values are: `New`, `Open`, `In Progress`, `Additional Info`, `Resolved`.

**Fix:** Replace the Status row accepted values with:
```
New | Open | In Progress | Additional Info | Resolved
Unrecognised values (including "Closed") default to New.
```

### G3-D — Priority (HD-TI-021)
**Current modal lists:** `Normal, Medium, High, Urgent, Rush`

**Required:** Only `Normal`, `Medium`, `Rush` are the business-facing accepted values (the internal coercion also maps High/Urgent → Rush, but the public-facing rule must only list Normal, Medium, Rush).

**Fix:** Replace Priority row with:
```
Normal | Medium | Rush
Unrecognised values default to Normal.
```
Also update `coercePriority()` to ONLY accept `normal`, `medium`, `rush` (and their numeric equivalents `1`, `2`, `3`). Remove `high`, `urgent`, `critical` and instead add a warning for those unrecognised values.

### G3-E — Channel (HD-TI-025)
**Current:** "Exact name of a Channel configured for your company (case-insensitive). Unmatched values are skipped with a warning; the ticket is still imported."

**Required:** Must state where channels can be reviewed.

**Fix:**
```
Exact name of a Channel configured for your company (case-insensitive).
Channels can be reviewed on the Helpdesk Ticket Settings page.
Unmatched values are skipped with a warning; the ticket is still imported without a channel.
```

### G3-F — Merchant Visibility (HD-TI-028)
**Current:** Shows `1 / yes / true / visible → visible to merchant. Any other value → hidden.`

**Required:** Clearly display the two allowed options as `Not Visible` and `Visible`.

**Fix:**
```
Not Visible (default when blank) | Visible
Accepted values for Visible: Visible, Yes, 1, True (case-insensitive).
Any other value or blank defaults to Not Visible.
```

**Files:** `resources/views/admin/helpdesk/ticket-importer.blade.php`

---

## G4 — Import Coercion & Validation Logic

### G4-A — Ticket Type valid/invalid handling (HD-TI-011, HD-TI-012)
**Current:** Invalid ticket type adds a `severity:'warning'` entry and continues. Valid ticket type maps to FK correctly.

**Status:** Functionally correct. These tests should pass once G3-A modal content is fixed and the result error table is verified to display warnings.

**Verify:** Confirm `ticket-importer-result.blade.php` renders the error table including `severity:'warning'` rows, not just `severity:'error'` rows.

### G4-B — Ticket Outcome valid/invalid handling (HD-TI-014)
Same as G4-A but for `outcome_label`. Verify warnings appear in result view.

### G4-C — External ID allows alphanumeric (HD-TI-015)
**Current:** `external_id` is accepted as any string. No format restriction enforced in `execute()`.

**Status:** Already works. Modal already documents it as "Alphanumeric identifier from your source system."

**No code change needed.**

### G4-D — Duplicate External ID handling (HD-TI-016)
**Current:** When a row is a duplicate (matched by subject OR external_id) and strategy is `skip`, the row is silently `$skipped++` with no entry in `$errors`.

**Required:** "System should either prevent duplicate import or show clear duplicate handling message."

**Fix:** When a row is skipped due to duplication, add a warning entry so the result view shows which rows were skipped and why:
```php
// In execute(), inside the $strategy === 'skip' block:
$skipped++;
$errors[] = [
    'row'      => $rowNum,
    'field'    => 'subject',
    'msg'      => 'Duplicate ticket skipped (matched by subject' . (!empty($extId) ? ' or External ID' : '') . ').',
    'severity' => 'warning',
];
continue;
```

**Files:** `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php` (`execute()` method)

### G4-E — Status coercion: "Closed" must be rejected (HD-TI-018, HD-TI-019, HD-TI-020)

**HD-TI-019 root cause:** `Helper::coerceStatus()` currently maps `str_contains($v, 'clos') → 5` (Resolved). The test requires `Closed` to be rejected (not a valid import status).

**HD-TI-020 root cause:** Case-insensitive matching via `strtolower()` is already applied, so `OPEN`, `In Progress`, `new` etc. should resolve correctly. Verify all allowed values work after the fix.

**Fix in `Helper::coerceStatus()`:**
```php
public static function coerceStatus(?string $val): int
{
    if (empty($val)) {
        return 1; // Default: New
    }
    $v = strtolower(trim($val));
    if (str_contains($v, 'resolv')) {
        return 5; // Resolved
    }
    if (str_contains($v, 'additional') || str_contains($v, 'info required')) {
        return 4; // Additional Info
    }
    if (str_contains($v, 'in progress') || str_contains($v, 'inprogress')) {
        return 3; // In Progress
    }
    if ($v === 'open') {
        return 2; // Open
    }
    return 1; // Default: New (also handles "Closed" → default New, caller adds warning)
}
```

Also update the **caller** in `execute()` to detect unrecognised status and log a warning:
```php
// After resolving status:
$rawStatus = trim($input['status'] ?? '');
$input['status'] = Helper::coerceStatus($rawStatus ?: null);
if (!empty($rawStatus)) {
    $knownStatuses = ['new', 'open', 'in progress', 'inprogress', 'additional', 'info required', 'resolv'];
    $isKnown = collect($knownStatuses)->some(fn($k) => str_contains(strtolower($rawStatus), $k));
    if (!$isKnown) {
        $errors[] = ['row' => $rowNum, 'field' => 'status', 'msg' => "Status \"{$rawStatus}\" is not a recognised value — defaulted to New.", 'severity' => 'warning'];
    }
}
```

**Files:** `app/Helpers/Helper.php`, `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php`

### G4-F — Priority: restrict accepted values to Normal/Medium/Rush (HD-TI-022, HD-TI-023, HD-TI-024)

**HD-TI-024 root cause:** `coercePriority()` silently defaults invalid values to Normal with no warning.

**Fix in `Helper::coercePriority()`:**
```php
public static function coercePriority(?string $val): int
{
    if (empty($val)) {
        return 1; // Default: Normal
    }
    $v = strtolower(trim($val));
    if (in_array($v, ['3', 'rush'])) {
        return 3;
    }
    if (in_array($v, ['2', 'medium'])) {
        return 2;
    }
    if (in_array($v, ['1', 'normal'])) {
        return 1;
    }
    return 0; // Sentinel: unrecognised — caller logs warning
}
```

Update the **caller** in `execute()`:
```php
$rawPriority = trim($input['priority'] ?? '');
$coercedPriority = Helper::coercePriority($rawPriority ?: null);
if ($coercedPriority === 0) {
    $errors[] = ['row' => $rowNum, 'field' => 'priority', 'msg' => "Priority \"{$rawPriority}\" is not valid (allowed: Normal, Medium, Rush) — defaulted to Normal.", 'severity' => 'warning'];
    $input['priority'] = 1;
} else {
    $input['priority'] = $coercedPriority;
}
```

**Files:** `app/Helpers/Helper.php`, `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php`

### G4-G — Channel valid/invalid (HD-TI-026, HD-TI-027)
**Current:** Invalid channel adds warning and continues. Valid channel maps to FK. Functionally correct.

**Status:** Already works. These tests should pass once G3-E modal content is fixed.

### G4-H — Merchant Visibility defaults (HD-TI-029, HD-TI-030)
**Current:** In `execute()`, if `merchant_visibility` is blank/unmapped, the field is omitted from `$input`. After `array_filter()`, it is stripped. The DB column default for `merchant_visibility` must be `0` (Not Visible) for the default-to-not-visible behavior.

**Fix:** Verify the `merchant_helpdesk_tickets` table column default for `merchant_visibility` is `0` (or `false`). If not, add a migration:
```php
$table->boolean('merchant_visibility')->default(false)->change();
```

Also ensure `merchant_visibility` IS included in the `$allowed` columns list when blank (set to 0 explicitly):
```php
// After array_filter, set explicit default for merchant_visibility if not already set:
$input['merchant_visibility'] = $input['merchant_visibility'] ?? 0;
```

**Files:** `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php`, migration if needed

---

## G5 — Assign To + User Attribution Fields (HD-TI-031–039)

### G5-A — Assign To: user ID lookup (HD-TI-032)
**Current:** Looks up by email, then by `first_name + last_name`. Numeric user IDs not supported.

**Fix:** Before email/name lookup, check if value is numeric and look up by primary key within same company:
```php
$assignedUserIds = []; // Will be array for multi-user support (G5-C)
if (!empty($input['assigned_to_label'])) {
    $vals = array_map('trim', explode(',', $input['assigned_to_label']));
    // Max 20 users validation (HD-TI-034)
    if (count($vals) > 20) {
        $errors[] = ['row' => $rowNum, 'field' => 'assigned_to_label', 'msg' => 'Assign To exceeds maximum of 20 users — first 20 used.', 'severity' => 'warning'];
        $vals = array_slice($vals, 0, 20);
    }
    foreach ($vals as $val) {
        $uid = null;
        $key = strtolower($val);
        if (is_numeric($val)) {
            // HD-TI-032: numeric user ID lookup
            $u = User::where('company_id', $companyId)->where('id', (int)$val)->whereNull('deleted_at')->first();
            $uid = $u ? $u->id : null;
        } elseif (isset($userByEmail[$key])) {
            $uid = $userByEmail[$key];
        } elseif (isset($userByName[$key])) {
            $uid = $userByName[$key];
        }
        if ($uid) {
            $assignedUserIds[] = $uid;
        } else {
            $errors[] = ['row' => $rowNum, 'field' => 'assigned_to_label', 'msg' => "Assigned To user \"{$val}\" not found — skipped.", 'severity' => 'warning'];
        }
    }
}
unset($input['assigned_to_label']);
```

### G5-B — Assign To: multiple comma-separated users (HD-TI-033)
Handled in G5-A above (splitting by comma, creating multiple `MerchantHelpdeskTicketUserMap` entries).

After ticket creation:
```php
foreach ($assignedUserIds as $uid) {
    MerchantHelpdeskTicketUserMap::firstOrCreate([
        'merchant_helpdesk_ticket_id' => $ticket->id,
        'user_id'                     => $uid,
        'company_id'                  => $companyId,
    ], ['is_notified' => false]);
}
```

### G5-C — Assign To: max 20 users (HD-TI-034)
Handled in G5-A above.

### G5-D — Blank Assign To → ticket type auto-assign users (HD-TI-035)
**Current:** Blank `assigned_to_label` → `$assignedUserIds` is empty → no assignment created.

**Required:** When blank AND a ticket type is resolved, look up the ticket type's auto-assign users and assign them.

**Pre-load in execute() (before row loop):**
```php
// Pre-load ticket type auto-assign users keyed by ticket_type_id
$ticketTypeAutoAssignUsers = \App\Models\Masters\MasterHelpdeskTicketType::where('company_id', $companyId)
    ->whereNull('deleted_at')
    ->with('autoAssignUsers') // assumes HasMany or BelongsToMany relation to users
    ->get()
    ->mapWithKeys(fn($tt) => [$tt->id => $tt->autoAssignUsers->pluck('id')->toArray()])
    ->all();
```

**In the assignment block (after ticket creation):**
```php
// If no users explicitly assigned, use ticket type auto-assign (HD-TI-035)
if (empty($assignedUserIds) && !empty($input['master_helpdesk_ticket_type_id'])) {
    $assignedUserIds = $ticketTypeAutoAssignUsers[$input['master_helpdesk_ticket_type_id']] ?? [];
}
```

> **Note:** Verify the correct relation name on `MasterHelpdeskTicketType` for auto-assign users (check the `master_helpdesk_ticket_type_user_maps` or equivalent pivot table). Update the `with()` clause accordingly.

### G5-E — Created By / Last Modified By / Last Status Update By fields (HD-TI-036, HD-TI-037, HD-TI-038, HD-TI-039)

**Current:** These user-attribution fields are not importable — they don't appear in `HelpdeskTicketImportJob::targetFields()` and are not processed in `execute()`.

**Required:** Add three new importable user-attribution fields.

**Step 1 — Add to `targetFields()` in `HelpdeskTicketImportJob`:**
```php
'created_by_label'            => ['label' => 'Created By',             'required' => false],
'last_modified_by_label'      => ['label' => 'Last Modified By',       'required' => false],
'last_status_updated_by_label' => ['label' => 'Last Status Update By', 'required' => false],
```

**Step 2 — Resolve in `execute()` (after existing user lookups, same pattern as Assign To):**

Add a helper closure in execute():
```php
$resolveUser = function(string $val) use ($companyId, $userByEmail, $userByName): ?int {
    $key = strtolower(trim($val));
    if (is_numeric($val)) {
        $u = User::where('company_id', $companyId)->where('id', (int)$val)->whereNull('deleted_at')->first();
        return $u ? $u->id : null;
    }
    return $userByEmail[$key] ?? $userByName[$key] ?? null;
};
```

In the row loop, after the existing relational lookups:
```php
// Created By (HD-TI-036, HD-TI-037)
$createdByUserId = Auth::id(); // default: importing user (HD-TI-038)
if (!empty($input['created_by_label'])) {
    $resolved = $resolveUser($input['created_by_label']);
    if ($resolved) {
        $createdByUserId = $resolved;
    } else {
        $errors[] = ['row' => $rowNum, 'field' => 'created_by_label', 'msg' => "Created By user \"{$input['created_by_label']}\" not found — defaulted to importing user.", 'severity' => 'warning'];
    }
}
unset($input['created_by_label']);

// Last Modified By
$lastModifiedByUserId = Auth::id();
if (!empty($input['last_modified_by_label'])) {
    $resolved = $resolveUser($input['last_modified_by_label']);
    if ($resolved) {
        $lastModifiedByUserId = $resolved;
    } else {
        $errors[] = ['row' => $rowNum, 'field' => 'last_modified_by_label', 'msg' => "Last Modified By user \"{$input['last_modified_by_label']}\" not found — defaulted to importing user.", 'severity' => 'warning'];
    }
}
unset($input['last_modified_by_label']);

// Last Status Update By
$lastStatusUpdatedByUserId = Auth::id();
if (!empty($input['last_status_updated_by_label'])) {
    $resolved = $resolveUser($input['last_status_updated_by_label']);
    if ($resolved) {
        $lastStatusUpdatedByUserId = $resolved;
    } else {
        $errors[] = ['row' => $rowNum, 'field' => 'last_status_updated_by_label', 'msg' => "Last Status Update By user \"{$input['last_status_updated_by_label']}\" not found — defaulted to importing user.", 'severity' => 'warning'];
    }
}
unset($input['last_status_updated_by_label']);
```

Then pass to `create()`:
```php
$ticket = MerchantHelpdeskTicket::create(array_merge($input, [
    'company_id'                    => $companyId,
    'created_by_user_id'            => $createdByUserId,
    'updated_by_user_id'            => $lastModifiedByUserId,
    'last_status_updated_by_user_id' => $lastStatusUpdatedByUserId,
]));
```

> **Note:** Verify the exact column names `updated_by_user_id` and `last_status_updated_by_user_id` exist on the `merchant_helpdesk_tickets` table and are in `$fillable` on `MerchantHelpdeskTicket`.

Also add to the validation rules modal (ticket-importer.blade.php):
```html
<tr>
    <td><strong>Created By</strong></td>
    <td><span class="badge bg-secondary">Optional</span></td>
    <td>
        User's email, full name, or numeric User ID.
        <span class="text-muted small">Defaults to the importing user when blank. Invalid values log a warning and default to the importing user.</span>
    </td>
</tr>
<tr>
    <td><strong>Last Modified By</strong></td>
    <td><span class="badge bg-secondary">Optional</span></td>
    <td>User's email, full name, or numeric User ID. Defaults to importing user when blank.</td>
</tr>
<tr>
    <td><strong>Last Status Update By</strong></td>
    <td><span class="badge bg-secondary">Optional</span></td>
    <td>User's email, full name, or numeric User ID. Defaults to importing user when blank.</td>
</tr>
```

**Files:** `app/Models/Helpdesk/HelpdeskTicketImportJob.php`, `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php`, `resources/views/admin/helpdesk/ticket-importer.blade.php`

---

## G6 — Date / Time Format Handling (HD-TI-040–047)

### Root Cause
`Helper::coerceDate()` uses `Carbon::parse($val)` which handles many formats, but is **ambiguous for `dd/mm/yyyy`**. For example, `Carbon::parse('30/06/2025')` interprets `30` as the month, which fails because month 30 doesn't exist, returning null and logging a false-positive parse error. The system needs explicit multi-format date parsing.

### Required Supported Formats
| Format | Example | Test |
|--------|---------|------|
| `d/m/Y` (dd/mm/yyyy) | 30/06/2025 | HD-TI-041 |
| `m/d/Y` (mm/dd/yyyy) | 06/30/2025 | HD-TI-042 |
| `Y-m-d` (yyyy-mm-dd) | 2025-06-30 | HD-TI-043 |
| `d/m/Y H:i` (with 24h time) | 30/06/2025 15:45 | HD-TI-041+046 |
| `d/m/Y g:i A` (with 12h time) | 30/06/2025 3:45 PM | HD-TI-041+045 |
| `Y-m-d H:i:s` (full datetime) | 2025-06-30 15:45:00 | HD-TI-043 |
| `Y-m-d\TH:i:s` (ISO 8601) | 2025-06-30T15:45:00 | — |
| `j M Y` (natural) | 30 Jun 2025 | — |

Unsupported → warning, field skipped (HD-TI-044, HD-TI-047).

### Fix — `Helper::coerceDate()`
```php
public static function coerceDate(?string $val): ?string
{
    if (empty($val)) {
        return null;
    }
    $val = trim($val);

    // Explicit format list in priority order
    $formats = [
        'Y-m-d H:i:s',
        'Y-m-d\TH:i:s',
        'Y-m-d H:i',
        'Y-m-d',
        'd/m/Y H:i:s',
        'd/m/Y H:i',
        'd/m/Y g:i A',
        'd/m/Y g:i a',
        'd/m/Y',
        'm/d/Y H:i:s',
        'm/d/Y H:i',
        'm/d/Y g:i A',
        'm/d/Y g:i a',
        'm/d/Y',
        'j M Y',
        'd M Y',
    ];

    foreach ($formats as $fmt) {
        try {
            $dt = \Carbon\Carbon::createFromFormat($fmt, $val);
            if ($dt && $dt->format($fmt) === $val || $dt !== false) {
                return $dt->format('Y-m-d H:i:s');
            }
        } catch (\Exception $e) {
            continue;
        }
    }

    // Last resort: try Carbon::parse for natural language dates
    try {
        return \Carbon\Carbon::parse($val)->format('Y-m-d H:i:s');
    } catch (\Exception $e) {
        return null; // Caller logs the warning
    }
}
```

> **Note on ambiguity:** `06/07/2025` is ambiguous — it could be June 7 (m/d/Y) or July 6 (d/m/Y). The format list above prioritises `d/m/Y` first. Document this in the validation rules modal and in any user-facing help text.

### HD-TI-040 — Date fields default to import datetime
When date columns are blank/unmapped, they are not included in `$input`. Eloquent sets `created_at`/`updated_at` to the current timestamp automatically. Custom date fields (`due_at`, `last_modified_at`, `last_status_updated_at`) will be null — this is the expected behavior.

**No code change needed** for default behavior. Verify DB columns allow null.

### HD-TI-045/046 — Time format support
Time formats `h:mm A` and `HH:mm` are only relevant when combined with a date value in a date-time field (e.g., `30/06/2025 3:45 PM`). The format list above in `coerceDate()` already covers these combinations. Standalone time values (without a date) are not a distinct importable field.

Update the validation rules modal — add time format info to the Created, Last Modified, Last Status Update date rows:
```
Same date formats as Due Date. Time may optionally be included: e.g., 30/06/2025 15:45 or 30/06/2025 3:45 PM.
```

**Files:** `app/Helpers/Helper.php`, `resources/views/admin/helpdesk/ticket-importer.blade.php`

---

## G7 — Import History Navigation (HD-TI-050)

### Root Cause
The "View History" button in `ticket-importer.blade.php` (line 16) correctly links to `route('helpdesk-ticket-import.history')`. The history() controller method and `ticket-importer-history.blade.php` view both appear complete.

**Investigate:** Check that `$this->_offset` in the `history()` method produces a valid paginate argument (must be > 0). If `_offset` is 0 or null, `paginate(0)` will throw. Inspect the parent `Controller::__construct()` to verify `$this->_offset` is initialized.

**Fix if needed:** In `history()`, use a hardcoded fallback:
```php
$perPage = ($this->_offset > 0) ? $this->_offset : 15;
$jobs = HelpdeskTicketImportJob::where('company_id', $companyId)
    ->orderByDesc('created_at')
    ->paginate($perPage);
```

Also verify that `$this->_data['breadcrumb']` is set in `history()` (for consistency with HD-TI-002 fix).

**Files:** `app/Http/Controllers/Helpdesk/HelpdeskTicketImportController.php`

---

## Implementation Order

| Priority | Group | Why first |
|----------|-------|-----------|
| 1 | G3 (modal content) | Fixes 6 tests, zero risk, UI only |
| 2 | G2 (button CSS) | Zero risk, 1 test |
| 3 | G1 (breadcrumb) | Global layout change, test in isolation |
| 4 | G4-E (status coercion) | Core logic change, affects all imports |
| 5 | G4-F (priority coercion) | Core logic change |
| 6 | G4-D (duplicate warning message) | Additive only |
| 7 | G4-H (merchant visibility default) | Check DB default first |
| 8 | G6 (date parsing) | Core logic, wide impact |
| 9 | G5-A/B/C (multi-user assign to) | Largest code change |
| 10 | G5-D (auto-assign fallback) | Depends on relation verification |
| 11 | G5-E (user attribution fields) | New target fields + migration risk |
| 12 | G7 (history) | Investigate `_offset` |

---

## Test Case Coverage Map

| Test ID | Fix Group | File(s) |
|---------|-----------|---------|
| HD-TI-002 | G1 | layout.blade.php |
| HD-TI-004 | G2 | ticket-importer.blade.php |
| HD-TI-010 | G3-A | ticket-importer.blade.php |
| HD-TI-011 | G4-A | ticket-importer-result.blade.php |
| HD-TI-012 | G4-A | HelpdeskTicketImportController.php |
| HD-TI-013 | G3-B | ticket-importer.blade.php |
| HD-TI-014 | G4-B | HelpdeskTicketImportController.php |
| HD-TI-015 | G4-C | No change needed |
| HD-TI-016 | G4-D | HelpdeskTicketImportController.php |
| HD-TI-017 | G3-C | ticket-importer.blade.php |
| HD-TI-018 | G4-E | Helper.php |
| HD-TI-019 | G4-E | Helper.php, Controller |
| HD-TI-020 | G4-E | Helper.php |
| HD-TI-021 | G3-D | ticket-importer.blade.php |
| HD-TI-022 | G4-F | Helper.php |
| HD-TI-023 | G4-F | Helper.php |
| HD-TI-024 | G4-F | Helper.php, Controller |
| HD-TI-025 | G3-E | ticket-importer.blade.php |
| HD-TI-026 | G4-G | No change needed |
| HD-TI-027 | G4-G | HelpdeskTicketImportController.php |
| HD-TI-028 | G3-F | ticket-importer.blade.php |
| HD-TI-029 | G4-H | Controller, migration |
| HD-TI-030 | G4-H | HelpdeskTicketImportController.php |
| HD-TI-031 | G5-A | HelpdeskTicketImportController.php |
| HD-TI-032 | G5-A | HelpdeskTicketImportController.php |
| HD-TI-033 | G5-B | HelpdeskTicketImportController.php |
| HD-TI-034 | G5-C | HelpdeskTicketImportController.php |
| HD-TI-035 | G5-D | HelpdeskTicketImportController.php |
| HD-TI-036 | G5-E | Controller, HelpdeskTicketImportJob.php |
| HD-TI-037 | G5-E | HelpdeskTicketImportController.php |
| HD-TI-038 | G5-E | HelpdeskTicketImportController.php |
| HD-TI-039 | G5-E | HelpdeskTicketImportController.php |
| HD-TI-040 | G6 | Helper.php |
| HD-TI-041 | G6 | Helper.php |
| HD-TI-042 | G6 | Helper.php |
| HD-TI-043 | G6 | Helper.php |
| HD-TI-044 | G6 | Helper.php, Controller |
| HD-TI-045 | G6 | Helper.php |
| HD-TI-046 | G6 | Helper.php |
| HD-TI-047 | G6 | Helper.php |
| HD-TI-050 | G7 | HelpdeskTicketImportController.php |
