# Plan: Capture UOI from Allmakes PSP and store as Min Order Qty

## Background

The Allmakes PSP scraper makes a single GET request per SKU:
`GET /parts/result?term={sku}`

This returns a search results table. Currently parsed fields: stock status, price, description, vendor.

The **UOI** (Unit of Issue) field appears in the product detail popup (shown when you click a product code in the PSP UI). That popup is almost certainly loaded via a **separate AJAX request** to a detail endpoint.

UOI is the minimum number of units you must order — it should be written to `products_product_suppliers.min_qty` for the Allmakes supplier record.

---

## Step 1 — Identify where UOI comes from

**Before writing any code**, visit the Allmakes PSP site in the browser with DevTools → Network tab open. Click a product code to open the popup, and watch for XHR/fetch requests.

You're looking for something like:
- `GET /parts/detail/STC468`
- `GET /parts/product?code=STC468`
- `POST /parts/modal`

Check the response — it will contain the Product Info table with UOI, Weight, Height, etc.

**Possible shortcut:** UOI may already be present in the search results HTML as a `data-` attribute on the row or the `data-pcode` element — check the raw HTML of `parts_search_table` first. If it's there, no second request is needed.

---

## Step 2 — Check the search results HTML

In `PspClient::parseResponse()`, a debug log already logs the first row HTML:
```php
Log::debug('AllmakesPsp ['.$sku.'] row HTML: '.mb_substr(..., 0, 2000));
```

Run a test scrape on a single SKU and check `storage/logs/laravel.log` for that row HTML. Look for `uoi`, `data-uoi`, or a 6th/7th table column containing the UOI value.

---

## Step 3 — Add a second HTTP request (if UOI not in search results)

If UOI requires a detail request, add a new method to `PspClient`:

```php
// In PspClient.php
private const DETAIL_PATH = '/parts/detail'; // verify actual path from Step 1

public function scrapeSkuDetail(string $sku, CookieJar $jar): ?int
{
    try {
        $response = $this->guzzle->get(self::DETAIL_PATH . '/' . urlencode($sku), [
            'cookies' => $jar,
            'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
        ]);

        $body = (string) $response->getBody()->getContents();
        if ($body === '') return null;

        return $this->parseUoi($body);
    } catch (\Throwable $e) {
        Log::warning("AllmakesPsp: UOI fetch failed for {$sku}: " . $e->getMessage());
        return null;
    }
}

private function parseUoi(string $html): ?int
{
    $dom = new \DOMDocument();
    libxml_use_internal_errors(true);
    $dom->loadHTML($html);
    libxml_clear_errors();

    $xpath = new \DOMXPath($dom);

    // Adjust XPath based on actual response HTML structure
    // e.g. a table row where the label cell contains "UOI"
    $nodes = $xpath->query('//*[contains(text(),"UOI")]/following-sibling::*[1]');

    if ($nodes->length > 0) {
        $val = trim($nodes->item(0)->textContent);
        return is_numeric($val) ? (int) $val : null;
    }

    return null;
}
```

**Rate limiting note:** If a second request is needed, add a small delay (100–200ms) between the search request and the detail request within `scrapeSku()`. The total per-SKU time will increase from ~500ms to ~700ms, making the full 9,087 SKU scrape ~106 minutes instead of ~85 minutes.

---

## Step 4 — Update ScrapeResult

Add `uoi` as a nullable int property:

```php
// ScrapeResult.php
class ScrapeResult
{
    public function __construct(
        public readonly string      $sku,
        public readonly StockStatus $status,
        public readonly ?float      $price       = null,
        public readonly ?string     $description = null,
        public readonly ?string     $vendor      = null,
        public readonly ?int        $uoi         = null,   // ADD THIS
        public readonly ?string     $errorMessage = null,
    ) {}
}
```

---

## Step 5 — Update scrapeSku() to fetch UOI

In `PspClient::scrapeSku()`, after parsing the search result, call the detail method if needed:

```php
public function scrapeSku(string $sku, CookieJar $jar): ScrapeResult
{
    // ... existing search request + parseResponse() ...

    $result = $this->parseResponse($sku, $body);

    // If in stock/transit, fetch UOI from detail endpoint
    if ($result->status->isAvailable()) {
        $uoi = $this->scrapeSkuDetail($sku, $jar);
        usleep(random_int(100_000, 200_000)); // polite gap before main delay
        return new ScrapeResult(
            $result->sku,
            $result->status,
            $result->price,
            $result->description,
            $result->vendor,
            $uoi,
        );
    }

    return $result;
}
```

If UOI IS in the search HTML (shortcut from Step 2), just parse it in `parseResponse()` instead.

---

## Step 6 — Add migration for psp_results table

```php
// New migration: add_uoi_to_allmakes_psp_results_table
Schema::table('allmakes_psp_results', function (Blueprint $table) {
    $table->unsignedSmallInteger('uoi')->nullable()->after('qty_set');
});
```

Register migration in the service provider's `hasMigrations([...])` array.

---

## Step 7 — Update PspResult model

Add `uoi` to `$fillable`:
```php
// Models/PspResult.php
protected $fillable = [
    ...,
    'uoi',
];
```

---

## Step 8 — Update saveResult() in ScrapeAllmakesPsp job

Pass UOI to `saveResult()` and write it to `products_product_suppliers.min_qty`:

```php
// In saveResult() signature:
private function saveResult(
    int $productId,
    string $sku,
    ?\Webkul\AllmakesPsp\Client\ScrapeResult $result,
    int $locationId,
    int $availableQty,
    ?string $errorMessage = null,
): void {
    // ...existing psp_results updateOrCreate — add uoi:
    PspResult::updateOrCreate(
        ['product_id' => $productId],
        [
            ...
            'uoi' => $result?->uoi,
        ]
    );

    // Write UOI to supplier min_qty if available
    if ($result?->uoi !== null && $result->uoi > 0) {
        DB::table('products_product_suppliers')
            ->where('product_id', $productId)
            ->whereExists(fn ($q) =>
                $q->from('partners_partners')
                  ->whereColumn('partners_partners.id', 'products_product_suppliers.partner_id')
                  ->where('partners_partners.name', 'like', 'Allmakes%')
            )
            ->update(['min_qty' => $result->uoi, 'updated_at' => now()]);
    }
}
```

---

## Step 9 — Show UOI in the dashboard table (optional)

Add a UOI column to the `am-tbl` in `allmakes-dashboard.blade.php` between Qty @ Allmakes and Last Checked. Pull `uoi` from the `psp_results` join in the `pagedResults` query in `AllmakesDashboard.php`.

---

## Files to touch (summary)

| File | Change |
|------|--------|
| `src/Client/PspClient.php` | Add `scrapeSkuDetail()`, `parseUoi()`, update `scrapeSku()` |
| `src/Client/ScrapeResult.php` | Add `?int $uoi` property |
| `src/Jobs/ScrapeAllmakesPsp.php` | Update `saveResult()` to write `min_qty` |
| `src/Models/PspResult.php` | Add `uoi` to `$fillable` |
| `database/migrations/xxxx_add_uoi_to_allmakes_psp_results.php` | New migration |
| `src/Providers/AllmakesPspServiceProvider.php` | Register new migration |
| `resources/views/.../allmakes-dashboard.blade.php` | (optional) Show UOI column |

---

## Checklist

- [ ] Step 1: Identify actual detail endpoint URL via browser DevTools
- [ ] Step 2: Check if UOI is already in search results HTML (saves a second request per SKU)
- [ ] Step 3: Add `scrapeSkuDetail()` to PspClient (if needed)
- [ ] Step 4: Add `uoi` to ScrapeResult
- [ ] Step 5: Wire up scrapeSku() to populate uoi
- [ ] Step 6: Write and run migration
- [ ] Step 7: Update PspResult model
- [ ] Step 8: Update saveResult() to write to min_qty
- [ ] Step 9: Add UOI column to dashboard table (optional)
