#!/usr/bin/env python3
"""
Phase II Backend Migration — ClickUp Task Creator
Creates all 36 tasks in the Phase II list.
"""

import json
import urllib.request
import urllib.error
import time
import sys

API_KEY = "pk_72611647_2LMGYGMR6XQDU21SDCPNUFAJUS7QAK0X"
LIST_ID = "901614081938"
BASE_URL = "https://api.clickup.com/api/v2"

# ─── Common blocks reused across tasks ───────────────────────────────────────

ENV_BLOCK = """## Environment
- **Project path:** `/home/centralgoparts/public_html` (Laravel 11 + Filament 3)
- **PHP binary:** `ea-php83` (use for ALL artisan commands)
- **Run commands:** `ea-php83 artisan {command}`
- **Branch:** `master`
- **Old Central path:** `/home/opsgoparts/www/central/` (Phalcon PHP framework — read only, do NOT modify)
- **Old and new Central share the same MySQL server and `goparts` database**
- **Additional databases on same server:** `goparts_ordering`, `goparts_magento` (cross-DB queries are fine)

## Reference Template — Follow This Pattern
Read this existing command first and follow its structure for logging, --dry-run, counters, Kernel registration, and error handling:
`/home/centralgoparts/public_html/app/Console/Commands/SyncShipEngineTracking.php`

## Existing Models (check before creating new ones)
```
ls /home/centralgoparts/public_html/app/Models/
```
Available: Order, OrderItem, OrderItemShipment, Shipment, Product, PartsSupplier, Supplier, MeyerOrder, OrderMonitoring, TrackingUpdate, FulfillmentOrder, Note, Notes, User, etc.

## Logging Standard (apply to ALL new commands)
1. Add a dedicated daily channel in `config/logging.php`
2. Use daily rotation, 14-day file retention
3. Log format: timestamp, action, identifiers, old→new values
4. Summary line at end of each run: total processed, updated, skipped, errors
5. Log channel name should match the artisan command name

## Verify After Implementation
1. `ea-php83 artisan {command} --dry-run` — confirm it lists what would change
2. `ea-php83 artisan schedule:list` — confirm it appears with correct frequency
3. Run side-by-side with old cron for 24h, compare DB changes
"""

MONITORING_BLOCK_TEMPLATE = """## Monitoring

### Log File
`storage/logs/{log_name}/{log_name}-YYYY-MM-DD.log` — daily rotation, 14-day retention

### Health Checks
- `ea-php83 artisan schedule:list` — verify task appears with correct schedule
- If 0 records processed for {zero_threshold} during business hours, investigate
- Compare DB state between old and new system during parallel run

### Post-Cutover
- Monitor for 1 week after disabling old cron entry
- {post_cutover_check}
"""

# ─── Task definitions ────────────────────────────────────────────────────────

TASKS = []

# ═══════════════════════════════════════════════════════════════════════════════
# INFRASTRUCTURE (Release first)
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-01: Laravel Scheduler & Queue Foundation",
    "tags": ["infrastructure", "phase-2"],
    "description": """## What
Set up the Laravel task scheduler cron entry for new Central, and configure the database queue driver to replace the old Phalcon DB-driven scheduler (`scheduler main`).

The old system uses a custom `SchedulerModel::runQueue()` that reads jobs from a `scheduler` DB table and executes them. Laravel has a built-in scheduler + queue system that is superior.

**Old cron:** `*/1 * * * * php cli.php scheduler main` (runs queued jobs from DB)
**New cron needed:** `* * * * * ea-php83 /home/centralgoparts/public_html/artisan schedule:run >> /dev/null 2>&1`

## Scope
### What to do
1. Add the Laravel scheduler cron entry to the `centralgoparts` crontab
2. Configure `config/queue.php` to use `database` driver instead of `sync`
3. Ensure `jobs`, `failed_jobs`, `job_batches` migration tables exist
4. Set up `queue:work` as a supervised process (or cron-based `queue:work --stop-when-empty`)
5. Verify `Kernel.php` schedule:run triggers all existing scheduled commands

### Old System Files (reference only)
- `/home/opsgoparts/www/central/app/tasks/SchedulerTask.php` — runs `SchedulerModel::runQueue()`
- `/home/opsgoparts/www/central/app/tasks/CronTask.php` — reads active jobs from `CronJob` DB table

### New System Files to Modify
- `centralgoparts` crontab — add `schedule:run` entry
- `/home/centralgoparts/public_html/config/queue.php` — change default to `database`
- `/home/centralgoparts/public_html/.env` — set `QUEUE_CONNECTION=database`

## Release Batch
**Infrastructure** — must be done FIRST before any other Phase 2 tasks

## Acceptance Criteria
- [ ] `* * * * * ea-php83 artisan schedule:run` is in `centralgoparts` crontab
- [ ] `ea-php83 artisan schedule:list` shows all 4 existing scheduled commands
- [ ] Queue driver set to `database` in `.env`
- [ ] `jobs`, `failed_jobs`, `job_batches` tables exist in DB
- [ ] `ea-php83 artisan queue:work --stop-when-empty` runs without error
- [ ] Existing scheduled tasks (shipping:cache-rates, monitoring:sync-tracking, etc.) still execute on schedule
- [ ] No interference with old Central's scheduler (they use different crontab users)

## Monitoring
- `ea-php83 artisan schedule:list` — shows all tasks with next run times
- `storage/logs/laravel.log` — check for scheduler errors
- `failed_jobs` table — should be empty after setup
- Verify each existing task ran at least once after setup

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## The Task
1. Check if the `centralgoparts` user crontab already has `schedule:run`. If not, add it:
   `* * * * * ea-php83 /home/centralgoparts/public_html/artisan schedule:run >> /dev/null 2>&1`
2. Change `.env` QUEUE_CONNECTION from `sync` to `database`
3. Run `ea-php83 artisan queue:table && ea-php83 artisan queue:failed-table && ea-php83 artisan queue:batches-table` if migrations don't exist, then `ea-php83 artisan migrate`
4. Verify `ea-php83 artisan schedule:list` shows all 4 existing tasks
5. Test `ea-php83 artisan queue:work --stop-when-empty` — should exit cleanly with no errors

Do NOT touch old Central. Do NOT disable old crontab.
"""
})

TASKS.append({
    "name": "PH2-02: Log Rotation & Housekeeping",
    "tags": ["infrastructure", "phase-2"],
    "description": """## What
Replace the old `daily_run.sh` shell script with a Laravel artisan command that handles log rotation, temp file cleanup, and archive management.

**Old cron:** `0 1 * * * /home/opsgoparts/public_html/central/shell/daily_run.sh`

The old script:
- Gzips price update logs older than 2 days, moves to archive
- Gzips price import logs older than 2 days, deletes gz files older than 7 days
- Deletes temp files older than 14 days
- Deletes session files older than 21 days
- Gzips price CSV files older than 30 days, deletes archives older than 60 days

## Scope
### Old System Files
- `/home/opsgoparts/public_html/central/shell/daily_run.sh` — 9 find/gzip/mv commands

### New System Files to Create
- `app/Console/Commands/SystemHousekeeping.php` — replaces daily_run.sh
- Register in `Kernel.php` — daily at 01:00

### What the Command Must Do
1. Rotate and archive old log files in `storage/logs/` subdirectories (gzip files > 14 days, delete > 60 days)
2. Clean `storage/framework/sessions/` — delete files > 21 days
3. Clean `storage/app/tmp/` or equivalent temp dir — delete files > 14 days
4. Run Laravel's built-in `log:clear` for the main log if needed
5. Report what was cleaned in its own log file

## Release Batch
**Infrastructure** — deploy alongside PH2-01

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan system:housekeeping`
- [ ] `--dry-run` flag shows what would be cleaned without deleting
- [ ] Registered in Kernel.php: `dailyAt('01:00')`
- [ ] Cleans: old logs (gzip > 14d, delete > 60d), sessions (> 21d), temp files (> 14d)
- [ ] Logs to `storage/logs/housekeeping/housekeeping-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: files compressed, files deleted, space freed

## Monitoring
- Log: `storage/logs/housekeeping/housekeeping-YYYY-MM-DD.log`
- Check disk usage trend — should stabilize after first week
- `ea-php83 artisan schedule:list` shows task

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
Read: `/home/opsgoparts/public_html/central/shell/daily_run.sh`
This is a bash script with find/gzip/mv commands. Rewrite all logic in PHP/Laravel.

## Create: app/Console/Commands/SystemHousekeeping.php
Signature: `system:housekeeping {--dry-run}`

1. Use Symfony Finder component (ships with Laravel) for file discovery
2. Process storage/logs/ subdirs: gzip .log files older than 14 days, delete .gz files older than 60 days
3. Clean storage/framework/sessions/ — delete sess_* files older than 21 days
4. Clean any temp directories — delete files older than 14 days
5. Log each action: file path, action (gzip/delete), file age, size
6. Summary at end: total gzipped, total deleted, total bytes freed
7. --dry-run: log what would happen, don't touch files
8. Register in Kernel.php: dailyAt('01:00')
9. Add logging channel 'housekeeping' in config/logging.php
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# ORDER OPERATIONS
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-03: Sync Shipped Orders with Magento",
    "tags": ["order-ops", "phase-2", "magento"],
    "description": """## What
Migrate the shipped order sync from old Central to new Central. This cron syncs order shipped status from Central back to Magento so the storefront reflects correct order statuses.

**Old cron:** `*/30 * * * * php cli.php order syncShippedOrders`
**Frequency:** Every 30 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` → `syncShippedOrdersAction()` (lines 156-166)
- `/home/opsgoparts/www/central/app/helpers/MagentoHelper.php` → `syncShippedOrders($days, $orderToSync)`
- Queries orders shipped in last 30 days, pushes status to Magento via MagentoHelper

### New System Files to Create
- `app/Console/Commands/SyncShippedOrders.php`
- Register in `Kernel.php`

### Key Business Logic
1. Calls `MagentoHelper::syncShippedOrders($days)` with default 30-day lookback
2. Finds orders in Central marked as shipped but not yet reflected in Magento
3. Updates Magento order status via direct DB or API

## Release Batch
**Order Operations** — release with PH2-04, PH2-05

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan orders:sync-shipped`
- [ ] `--dry-run` flag logs what would be synced without updating Magento
- [ ] `--days=N` option (default 30)
- [ ] Registered in Kernel.php: `everyThirtyMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/sync-shipped-orders/sync-shipped-orders-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each sync logs: order_id, external_order_id, status sent to Magento
- [ ] Summary: total found, synced, skipped, errors

## Monitoring
- Log: `storage/logs/sync-shipped-orders/`
- Health: If 0 orders synced for 4+ hours during business hours, investigate
- Magento verification: spot-check 5 recently shipped orders in Magento admin
- Post-cutover: watch for orders stuck in "Processing" in Magento that should be "Complete"

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` lines 156-166 → `syncShippedOrdersAction()`
- Trace into `/home/opsgoparts/www/central/app/helpers/MagentoHelper.php` → `syncShippedOrders()`
- Understand how it connects to Magento (direct DB query to `goparts_magento` or API call?)

## Create: app/Console/Commands/SyncShippedOrders.php
Signature: `orders:sync-shipped {--days=30} {--dry-run}`

1. Replicate the MagentoHelper::syncShippedOrders() logic
2. If Magento connection uses a separate DB, ensure connection config exists in config/database.php
3. --dry-run: log what would sync, don't update
4. Schedule: everyThirtyMinutes()->withoutOverlapping()
5. Log channel: 'sync-shipped-orders'
"""
})

TASKS.append({
    "name": "PH2-04: Sync Canceled Orders with Magento",
    "tags": ["order-ops", "phase-2", "magento"],
    "description": """## What
Migrate the canceled order sync from old Central to new Central. Syncs order cancellation status from Central back to Magento.

**Old cron:** `*/30 * * * * php cli.php order syncCanceledOrders`
**Frequency:** Every 30 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` → `syncCanceledOrdersAction()` (lines 171-180)
- `/home/opsgoparts/www/central/app/helpers/MagentoHelper.php` → `syncCanceledOrders($days)`
- Default 30-day lookback

### New System Files to Create
- `app/Console/Commands/SyncCanceledOrders.php`
- Register in `Kernel.php`

## Release Batch
**Order Operations** — release with PH2-03, PH2-05

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan orders:sync-canceled`
- [ ] `--dry-run` flag, `--days=N` option (default 30)
- [ ] Registered in Kernel.php: `everyThirtyMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/sync-canceled-orders/sync-canceled-orders-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: total found, synced, skipped, errors

## Monitoring
- Log: `storage/logs/sync-canceled-orders/`
- Compare: during parallel run, count cancellations pushed by old vs new
- Post-cutover: check for orders canceled in Central but still "Processing" in Magento

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` lines 171-180
- Trace into MagentoHelper::syncCanceledOrders()

## Create: app/Console/Commands/SyncCanceledOrders.php
Signature: `orders:sync-canceled {--days=30} {--dry-run}`

1. Replicate MagentoHelper::syncCanceledOrders() logic exactly
2. Schedule: everyThirtyMinutes()->withoutOverlapping()
3. Log channel: 'sync-canceled-orders'
"""
})

TASKS.append({
    "name": "PH2-05: Process Dropshipper Tracking from Feeds",
    "tags": ["order-ops", "phase-2", "tracking"],
    "description": """## What
Migrate the dropshipper tracking processor from old Central. This cron reads `dropshipper_tracking` records and matches them to order items, updating tracking numbers and item statuses.

**Old cron:** `*/15 * * * * php cli.php order processTracks`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` → `processTracksAction()` (lines 185-196)
- `/home/opsgoparts/www/central/app/helpers/MailParser.php` → `processOrders($tid, $debug)`
- Reads unprocessed records from `dropshipper_tracking` table
- Matches to order items by external_order_id + part_num
- Updates item tracking, status, and carrier info

### New System Files to Create
- `app/Console/Commands/ProcessDropshipperTracks.php`
- May need `app/Models/DropshipperTracking.php` if not exists

## Release Batch
**Order Operations** — release with PH2-03, PH2-04

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan orders:process-tracks`
- [ ] `--dry-run` flag, optional `--id=N` to process single record
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/process-tracks/process-tracks-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each processed record logs: tracking_id, external_order_id, tracking_number, matched_item_id, status change
- [ ] Summary: total processed, matched, unmatched, errors

## Monitoring
- Log: `storage/logs/process-tracks/`
- Health: `SELECT COUNT(*) FROM dropshipper_tracking WHERE processed = 0` should trend toward 0
- Post-cutover: watch for growing backlog of unprocessed tracking records

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/OrderTask.php` lines 185-196
- Trace into `/home/opsgoparts/www/central/app/helpers/MailParser.php` → `processOrders()`
- Understand the `dropshipper_tracking` table schema and the `processed` flag

## Create: app/Console/Commands/ProcessDropshipperTracks.php
Signature: `orders:process-tracks {--id= : Process single tracking record} {--dry-run}`

1. Read unprocessed dropshipper_tracking records (processed = 0)
2. Match to order items, update tracking/status
3. Mark as processed = 1 when done
4. Schedule: everyFifteenMinutes()->withoutOverlapping()
5. Log channel: 'process-tracks'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# USAUTO
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-06: USAuto Reports Generation",
    "tags": ["usauto", "phase-2", "reports"],
    "description": """## What
Migrate USAuto report generation from old Central. Generates reports about USAuto order fulfillment.

**Old cron:** `*/15 * * * * php cli.php usauto main`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/UsautoTask.php` → `mainAction()` (lines 12-19)
- `/home/opsgoparts/www/central/app/helpers/UsautoHelper.php` → `generateReports()`
- Generates reports about USAuto order status, shipping, no-tracking issues

### New System Files to Create
- `app/Console/Commands/UsautoGenerateReports.php`

## Release Batch
**USAuto** — release with PH2-07, PH2-08

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan usauto:generate-reports`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/usauto-reports/usauto-reports-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Reports generated match old system output

## Monitoring
- Log: `storage/logs/usauto-reports/`
- Verify reports are generated and accessible
- Post-cutover: confirm operations team sees same report data

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/UsautoTask.php` lines 12-19
- Trace into `/home/opsgoparts/www/central/app/helpers/UsautoHelper.php` → `generateReports()`
- Understand what reports are generated, where they are stored/emailed

## Create: app/Console/Commands/UsautoGenerateReports.php
Signature: `usauto:generate-reports {--dry-run}`

1. Replicate UsautoHelper::generateReports() logic
2. Schedule: everyFifteenMinutes()->withoutOverlapping()
3. Log channel: 'usauto-reports'
"""
})

TASKS.append({
    "name": "PH2-07: USAuto Tracking Sync",
    "tags": ["usauto", "phase-2", "tracking"],
    "description": """## What
Migrate USAuto tracking info sync from old Central. Reads tracking numbers from the `goparts_ordering` database and creates `dropshipper_tracking` records for USAuto (supplier_id=16).

**Old cron:** `*/2 * * * * php cli.php usauto updateTrackingInfoFromOrdering`
**Frequency:** Every 2 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/UsautoTask.php` → `updateTrackingInfoFromOrderingAction()` (lines 35-104)
- Self-contained — no external helper. Logic is inline:
  1. Queries `goparts_ordering.order_items` JOIN `goparts_ordering.orders` for items with tracking in last 20 days
  2. LEFT JOINs `goparts.dropshipper_tracking` — only rows where NO match exists (entity_id IS NULL)
  3. Inserts into `dropshipper_tracking`: supplier_id=16, carrier detected by regex, PO, tracking, part_num, qty
  4. Carrier detection: FedEx (12-digit or 96+13/18), USPS (9+19-21 digits or AA000000000US), default USPS

### New System Files to Create
- `app/Console/Commands/UsautoSyncTracking.php`
- `app/Models/DropshipperTracking.php` (if not exists)
- Add `goparts_ordering` DB connection in `config/database.php` if not present

## Release Batch
**USAuto** — release with PH2-06, PH2-08

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan usauto:sync-tracking`
- [ ] `--dry-run` flag, `--days=N` option (default 20)
- [ ] Registered in Kernel.php: `everyTwoMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/usauto-tracking-sync/usauto-tracking-sync-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each insert logs: PO, tracking_number, part_num, detected carrier
- [ ] Summary: total found, inserted, skipped, errors
- [ ] `goparts_ordering` DB connection configured
- [ ] Carrier regex matches old system exactly
- [ ] 24h side-by-side comparison shows identical records created

## Monitoring
- Log: `storage/logs/usauto-tracking-sync/`
- Health: if 0 records for 6+ consecutive runs during business hours, investigate
- Compare: `SELECT COUNT(*) FROM dropshipper_tracking WHERE supplier_id=16 AND created_at > NOW() - INTERVAL 1 HOUR`
- Post-cutover: watch for USAuto orders missing tracking in dropshipper_tracking

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/UsautoTask.php` lines 35-104

The exact SQL query (lines 38-57):
```sql
SELECT o.po, oi.supplier_part, oi.partslink, oi.quantity, oi.tracking_num, dt.entity_id
FROM goparts_ordering.order_items oi
LEFT JOIN goparts_ordering.orders o ON oi.order_id = o.id
LEFT JOIN goparts.dropshipper_tracking dt ON dt.external_order_id = o.po
    AND dt.tracking_number = oi.tracking_num
    AND dt.part_num = IFNULL(oi.supplier_part, oi.partslink)
WHERE oi.updated_at >= DATE_SUB(NOW(), INTERVAL {days} DAY)
    AND oi.tracking_num IS NOT NULL AND o.po IS NOT NULL AND dt.entity_id IS NULL
```

Carrier detection (lines 87-103):
- Strip non-alphanumeric chars
- FedEx: `/^(?:\\d{12}|96\\d{13}|96\\d{18})$/`
- USPS: `/^(?:9\\d{19,21}|[A-Z]{2}\\d{9}US)$/i`
- Default: USPS

## Create: app/Console/Commands/UsautoSyncTracking.php
Signature: `usauto:sync-tracking {--days=20} {--dry-run}`

1. Add goparts_ordering connection to config/database.php if missing (same host, use env vars)
2. Use raw SQL via DB::connection('goparts_ordering') for the cross-DB join
3. Create DropshipperTracking model if needed (table: dropshipper_tracking)
4. Port detectCarrier() regex exactly as-is
5. supplier_id is always 16 (USAuto)
6. Schedule: everyTwoMinutes()->withoutOverlapping()
7. Log channel: 'usauto-tracking-sync'
"""
})

TASKS.append({
    "name": "PH2-08: USAuto Price Import",
    "tags": ["usauto", "phase-2", "price"],
    "description": """## What
Migrate the USAuto price import pipeline from a shell script to a Laravel artisan command. Currently a bash script that unzips price files, converts them, and imports them.

**Old cron:** `15 22 * * * /home/opsgoparts/public_html/central/shell/usauto_price.sh`
**Frequency:** Daily at 10:15 PM

## Scope
### Old System Files
- `/home/opsgoparts/public_html/central/shell/usauto_price.sh` — orchestrates the pipeline:
  1. Unzips newest ZIP from `/home/usauto/usauto_inv/`
  2. Gets newest .txt file
  3. Calls `price convertFile '["USAVA", "/path/to/file.txt"]'` — converts to standard CSV
  4. Calls `price importPrice '["converted_file.csv"]'` — imports into parts_suppliers
  5. Cleans up files > 7 days
- `/home/opsgoparts/www/central/app/tasks/PriceTask.php` → `convertFileAction()` (lines 20-62), `importPriceAction()` (lines 67-128)
- `/home/opsgoparts/www/central/app/models/PriceImportModel.php` → `convertPriceFile()`, `import()`

### New System Files to Create
- `app/Console/Commands/UsautoImportPrices.php` — combines shell + PHP logic

## Release Batch
**USAuto** — release with PH2-06, PH2-07

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan usauto:import-prices`
- [ ] `--dry-run` flag, `--file=` to specify a file manually
- [ ] Registered in Kernel.php: `dailyAt('22:15')->withoutOverlapping()`
- [ ] Automatically finds newest ZIP in `/home/usauto/usauto_inv/`, extracts, converts, imports
- [ ] Cleans up files older than 7 days in source directory
- [ ] Logs to `storage/logs/usauto-price-import/usauto-price-import-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: file processed, records imported, records updated, errors
- [ ] Price data matches what old pipeline would produce for same input file

## Monitoring
- Log: `storage/logs/usauto-price-import/`
- Health: if no import for 48+ hours, alert — may indicate missing ZIP upload from USAuto
- Verify: spot-check 10 parts_suppliers records updated by import, compare prices to source file
- Post-cutover: run old and new on same ZIP file, compare import results

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
Shell script: `/home/opsgoparts/public_html/central/shell/usauto_price.sh`
```bash
ls -t1 /home/usauto/usauto_inv/*.zip | head -1 | xargs -n1 unzip -o -d /home/usauto/usauto_inv/
LATEST_FILE=$(ls -t1 /home/usauto/usauto_inv/*.txt | head -1)
PARAMS='["USAVA", "'$LATEST_FILE'"]'
CONVERTED_FILE=$(php cli.php price convertFile "$PARAMS")
php cli.php price importPrice '["'$CONVERTED_FILE'"]'
find /home/usauto/usauto_inv/ -name "*" -type f -mtime +7 -exec rm -f {} \\;
```

PHP tasks:
- `/home/opsgoparts/www/central/app/tasks/PriceTask.php` → convertFileAction() and importPriceAction()
- `/home/opsgoparts/www/central/app/models/PriceImportModel.php`

## Create: app/Console/Commands/UsautoImportPrices.php
Signature: `usauto:import-prices {--file= : Path to specific file} {--dry-run}`

1. Find newest ZIP in /home/usauto/usauto_inv/, extract with ZipArchive (PHP, not shell)
2. Find newest .txt file in extracted files
3. Port PriceImportModel::convertPriceFile() logic for supplier code "USAVA"
4. Port PriceImportModel::import() logic
5. Clean up files > 7 days
6. Schedule: dailyAt('22:15')->withoutOverlapping()
7. Log channel: 'usauto-price-import'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# PARTSQUARE
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-09: PartSquare Order Processing",
    "tags": ["partsquare", "phase-2", "orders"],
    "description": """## What
Migrate PartSquare (PS) order processing from old Central. Submits PS orders to ShipStation for fulfillment.

**Old cron:** `*/10 * * * * php cli.php psorders processOrders`
**Frequency:** Every 10 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PsordersTask.php` → `processOrdersAction()` (lines 26-61)
- Uses file-based locking (5-minute TTL) for overlap protection
- `/home/opsgoparts/www/central/app/helpers/PsOrderHelper.php` → `processOrders()`
- DB model: `PsOrders` — table `ps_orders`

### New System Files to Create
- `app/Console/Commands/PsProcessOrders.php`
- `app/Models/PsOrder.php` (if not exists)

## Release Batch
**PartSquare** — release with PH2-10, PH2-11

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan ps:process-orders`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `everyTenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/ps-process-orders/ps-process-orders-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: total orders, processed, errors
- [ ] Uses Laravel's withoutOverlapping() instead of manual file lock

## Monitoring
- Log: `storage/logs/ps-process-orders/`
- Health: `SELECT COUNT(*) FROM ps_orders WHERE status = 'pending'` should not grow unboundedly
- Post-cutover: verify PS orders are being submitted to ShipStation correctly

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PsordersTask.php` lines 26-61
- Trace into PsOrderHelper::processOrders()
- Note: old code uses manual file-based lock — replace with Laravel's withoutOverlapping()

## Create: app/Console/Commands/PsProcessOrders.php
Signature: `ps:process-orders {--dry-run}`

1. Replicate PsOrderHelper::processOrders() logic
2. Remove manual file-lock, use withoutOverlapping() instead
3. Schedule: everyTenMinutes()->withoutOverlapping()
4. Log channel: 'ps-process-orders'
"""
})

TASKS.append({
    "name": "PH2-10: PartSquare Tracking Sync",
    "tags": ["partsquare", "phase-2", "tracking"],
    "description": """## What
Migrate PartSquare tracking sync from old Central. Pulls tracking info from PS API and updates PS order records.

**Old cron:** `*/15 * * * * php cli.php psorders syncPsTrackInfo`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PsordersTask.php` → `syncPsTrackInfoAction()` (lines 84-94)
- `/home/opsgoparts/www/central/app/helpers/PsOrderHelper.php` → `processTrackingInfoFromPS($days)` — default 90-day lookback

### New System Files to Create
- `app/Console/Commands/PsSyncTracking.php`

## Release Batch
**PartSquare** — release with PH2-09, PH2-11

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan ps:sync-tracking`
- [ ] `--dry-run` flag, `--days=N` (default 90)
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/ps-sync-tracking/ps-sync-tracking-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: total checked, updated, skipped, errors

## Monitoring
- Log: `storage/logs/ps-sync-tracking/`
- Verify PS orders get tracking numbers populated
- Post-cutover: check for PS orders stuck without tracking

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PsordersTask.php` lines 84-94
- Trace into PsOrderHelper::processTrackingInfoFromPS()

## Create: app/Console/Commands/PsSyncTracking.php
Signature: `ps:sync-tracking {--days=90} {--dry-run}`

1. Replicate PsOrderHelper::processTrackingInfoFromPS() logic
2. Schedule: everyFifteenMinutes()->withoutOverlapping()
3. Log channel: 'ps-sync-tracking'
"""
})

TASKS.append({
    "name": "PH2-11: PartSquare Vendor Table Build",
    "tags": ["partsquare", "phase-2", "catalog"],
    "description": """## What
Migrate the PartSquare vendor table builder from old Central. Rebuilds a lookup table used by PartSquare for vendor matching.

**Old cron:** `0 2 * * * php cli.php partscan createPartsquareVendorTable`
**Frequency:** Daily at 2:00 AM

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PartscanTask.php` → `createPartsquareVendorTableAction()` (lines 83-87)
- `/home/opsgoparts/www/central/app/helpers/PartscanInventoryHelper.php` → `createPartsquareVendorTable()`

### New System Files to Create
- `app/Console/Commands/PsBuildVendorTable.php`

## Release Batch
**PartSquare** — release with PH2-09, PH2-10

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan ps:build-vendor-table`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `dailyAt('02:00')`
- [ ] Logs to `storage/logs/ps-vendor-table/ps-vendor-table-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Vendor table row count matches old system output

## Monitoring
- Log: `storage/logs/ps-vendor-table/`
- Verify table is rebuilt nightly
- Post-cutover: compare row counts day over day

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PartscanTask.php` lines 83-87
- Trace into PartscanInventoryHelper::createPartsquareVendorTable()

## Create: app/Console/Commands/PsBuildVendorTable.php
Signature: `ps:build-vendor-table {--dry-run}`

1. Replicate PartscanInventoryHelper::createPartsquareVendorTable() logic
2. Schedule: dailyAt('02:00')
3. Log channel: 'ps-vendor-table'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# SHIPPING (ShipStation + ShipEngine)
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-12: ShipStation Carrier Rate Preloading — Evaluate Overlap",
    "tags": ["shipping", "phase-2", "evaluation"],
    "description": """## What
**EVALUATION TASK** — Determine if old Central's ShipStation carrier rate preloading is still needed, or if new Central's `shipping:cache-rates` already covers it.

**Old cron:** `*/2 * * * * php cli.php shipstation preloadCarrierRates`
**New Central already has:** `shipping:cache-rates` running every minute

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/ShipstationTask.php` → `preloadCarrierRatesAction()` (lines 24-33)
- Uses `Shipment_ShipStation_Carrier_Rate_Cron::run()`

### New System Already Has
- `app/Console/Commands/CacheShippingRates.php` — runs every minute, caches rates for recent orders

### Key Questions to Answer
1. Does `shipping:cache-rates` cover ALL the same rate lookups that `preloadCarrierRates` does?
2. Are there any rate scenarios covered by old but not new?
3. If there's overlap, can we safely skip this migration?
4. If there's a gap, what specific rates need to be added to the new command?

## Release Batch
**Shipping** — release with PH2-13, PH2-14

## Acceptance Criteria
- [ ] Written analysis document comparing old vs new rate caching
- [ ] Decision: SKIP (fully covered) or MIGRATE (gaps found)
- [ ] If MIGRATE: new command created with gap coverage
- [ ] If SKIP: documented why, with evidence

## Monitoring
- N/A if skipped
- If migrated: same pattern as other commands

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Evaluate Overlap
1. Read old: `/home/opsgoparts/www/central/app/tasks/ShipstationTask.php` lines 24-33
2. Trace into: `Shipment_ShipStation_Carrier_Rate_Cron::run()` — understand what rates it caches
3. Read new: `/home/centralgoparts/public_html/app/Console/Commands/CacheShippingRates.php`
4. Compare: what does old cache that new doesn't?
5. Write findings as comments in this task

Decision: If new covers everything → mark SKIP. If gaps → create a new command to fill them.
"""
})

TASKS.append({
    "name": "PH2-13: ShipStation Shipment Updates",
    "tags": ["shipping", "phase-2", "tracking"],
    "description": """## What
Migrate ShipStation shipment update sync from old Central. Polls ShipStation API for tracking numbers and shipping costs, updates shipment and order records.

**Old cron:** `5 * * * * php cli.php shipstation updateShipments`
**Frequency:** Hourly at :05

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/ShipstationTask.php` → `updateShipmentsAction()` (lines 47-181)
- Finds shipments from last 14 days with missing tracking_number or shipping_cost
- For each: calls ShipStation API per warehouse auth, gets tracking/cost/carrier/method/date
- Updates shipment record, then updates linked order items with tracking info
- Uses `ShipStation` library, `ShipmentHelper` for warehouse auth and carrier mapping

### New System Files to Create
- `app/Console/Commands/ShipstationUpdateShipments.php`
- May need ShipStation API library or use existing if installed

## Release Batch
**Shipping** — release with PH2-12, PH2-14

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan shipstation:update-shipments`
- [ ] `--dry-run` flag, `--days=N` (default 14)
- [ ] Registered in Kernel.php: `hourlyAt(5)->withoutOverlapping()`
- [ ] Logs to `storage/logs/shipstation-update-shipments/shipstation-update-shipments-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each update logs: shipment_id, external_shipment_id, tracking_number, carrier, cost, order_id
- [ ] Summary: total checked, updated, skipped (no tracking), errors
- [ ] ShipStation API auth configured per warehouse (check old WarehouseHelper config)

## Monitoring
- Log: `storage/logs/shipstation-update-shipments/`
- Health: if no shipments updated for 24+ hours, investigate ShipStation API or auth
- Post-cutover: verify shipment records have tracking populated within 1-2 hours of ShipStation label creation

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/ShipstationTask.php` lines 47-181 (full updateShipmentsAction)
- `/home/opsgoparts/www/central/app/helpers/ShipmentHelper.php` — getWarehouseData(), carrier/method mappings
- `/home/opsgoparts/www/central/app/library/Shipstation/ShipStation.php` — API client

Key logic:
1. Query shipments where (tracking_number IS NULL OR shipping_cost IS NULL) AND date_submitted >= -14 days
2. For each shipment: get warehouse auth, call ShipStation getShipments API by orderId
3. Update: tracking_number, shipping_cost, date_shipped, carrier, shipment_method
4. Then update linked order items via order->addItemsTrackingInfo()

## Create: app/Console/Commands/ShipstationUpdateShipments.php
Signature: `shipstation:update-shipments {--days=14} {--dry-run}`

1. Port the full updateShipmentsAction() logic
2. Ensure ShipStation API library exists (check composer.json or port from old)
3. Port warehouse auth mapping from ShipmentHelper
4. Port carrier/method code mappings (ShipmentHelper::$ssCarriers, ::$methods)
5. Schedule: hourlyAt(5)->withoutOverlapping()
6. Log channel: 'shipstation-update-shipments'
"""
})

TASKS.append({
    "name": "PH2-14: ShipEngine Batch Subscribe — Evaluate Overlap",
    "tags": ["shipping", "phase-2", "evaluation"],
    "description": """## What
**EVALUATION TASK** — The old Central has a ShipEngine batch subscribe cron. New Central already has `monitoring:sync-tracking` which polls ShipEngine. Determine if there's any gap.

**Old cron:** `*/10 * * * * php cli.php shipengine batchSubscribe`
**New Central already has:** `monitoring:sync-tracking` (every 2h) — polls ShipEngine for tracking updates
**Also disabled:** `shipengine updateOrdersIsFullDeliveredFlag` (disabled 2026-02-13, handled by new sync-tracking)

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/ShipengineTask.php` → `batchSubscribeAction()` (lines 19-34)
- Uses `ShipengineHelper::batchSubscribeOrdersItems()` — subscribes to webhook tracking updates

### New System Already Has
- `app/Console/Commands/SyncShipEngineTracking.php` — polling-based (no webhooks)

### Key Question
Old system uses webhook subscriptions, new system uses polling. Is polling sufficient, or do we need webhook subscriptions too for real-time updates?

## Release Batch
**Shipping** — release with PH2-12, PH2-13

## Acceptance Criteria
- [ ] Analysis: does polling every 2h meet business needs, or do we need real-time webhook updates?
- [ ] Decision: SKIP (polling sufficient) or MIGRATE (need webhook subscriptions)
- [ ] If SKIP: document why
- [ ] If MIGRATE: implement webhook subscription command

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Evaluate
1. Read old: `/home/opsgoparts/www/central/app/tasks/ShipengineTask.php` lines 19-34
2. Trace: ShipengineHelper::batchSubscribeOrdersItems() — what does subscribing do?
3. Read new: `/home/centralgoparts/public_html/app/Console/Commands/SyncShipEngineTracking.php`
4. Compare approaches: webhook subscription vs 2h polling
5. Decision: is the 2h polling gap acceptable for operations?
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# MEYER
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-15: Meyer Auto-Ordering",
    "tags": ["meyer", "phase-2", "orders"],
    "description": """## What
Migrate Meyer auto-ordering from old Central. Processes unprocessed Meyer orders by calling the Meyer API to place orders, then updates local records with Meyer order IDs.

**Old cron:** `*/15 * * * * php cli.php meyer processOrders`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/MeyerTask.php` → `processOrdersAction()` (lines 40-149)
- `/home/opsgoparts/www/central/app/helpers/MeyerApiHelper.php` → `createOrder($data)`
- DB model: `MeyerOrders` table — processed=0 means unprocessed
- Builds shipping data from Orders table, sends to Meyer API
- On success: sets processed=1, stores meyer_order_id
- On failure: sets processed=-1, stores error message
- Handles B2B vs B2C shipping name differences

### New System Files to Create
- `app/Console/Commands/MeyerProcessOrders.php`
- Check if `app/Models/MeyerOrder.php` exists (it does per Models listing)

## Release Batch
**Meyer** — release with PH2-16

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan meyer:process-orders`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/meyer-process-orders/meyer-process-orders-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each order logs: external_order_id, items, Meyer API response, success/failure
- [ ] Summary: total unprocessed, successfully ordered, failed, errors
- [ ] Handles B2B (source=500) company name logic exactly as old code
- [ ] State code conversion for long state names

## Monitoring
- Log: `storage/logs/meyer-process-orders/`
- Health: `SELECT COUNT(*) FROM meyer_orders WHERE processed = 0` should trend to 0
- Alert: if processed=-1 count grows, Meyer API may be rejecting orders
- Post-cutover: verify Meyer orders are being placed correctly by checking Meyer portal

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/MeyerTask.php` lines 40-149 (full processOrdersAction)
`/home/opsgoparts/www/central/app/helpers/MeyerApiHelper.php` → createOrder()
`/home/opsgoparts/www/central/app/helpers/CountryHelper.php` → convertUsaState()

Key logic:
1. Find DISTINCT external_order_id from meyer_orders WHERE processed=0
2. For each: build shipping data from Orders table (B2B: company name, B2C: first+last name)
3. Build items array from meyer_orders with supplier_partnumber + quantity
4. Call MeyerApiHelper::createOrder() — sets ShipMethod="MEYER CHOICE"
5. On success: set processed=1, store meyer_order_id per item
6. On failure: set processed=-1, store error

## Create: app/Console/Commands/MeyerProcessOrders.php
Signature: `meyer:process-orders {--dry-run}`

1. Port the full processOrdersAction() logic using existing MeyerOrder model
2. Port MeyerApiHelper for API calls (or create Laravel service)
3. Port CountryHelper::convertUsaState() if not available
4. Schedule: everyFifteenMinutes()->withoutOverlapping()
5. Log channel: 'meyer-process-orders'
"""
})

TASKS.append({
    "name": "PH2-16: Meyer Tracking Sync",
    "tags": ["meyer", "phase-2", "tracking"],
    "description": """## What
Migrate Meyer tracking sync from old Central. Polls Meyer API for tracking numbers on placed orders, updates order items with tracking and shipped status.

**Old cron:** `*/15 * * * * php cli.php meyer processTracking`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/MeyerTask.php` → `processTrackingAction()` (lines 150-196)
- Finds meyer_orders WHERE processed=1 (ordered but no tracking yet)
- Calls MeyerApiHelper::processTracking(meyer_order_id) for each
- On tracking found: updates OrdersItems.tracking, current_status=6 (Shipped), carrier
- Sets meyer_orders.processed=2 when tracking received

### New System Files to Create
- `app/Console/Commands/MeyerSyncTracking.php`

## Release Batch
**Meyer** — release with PH2-15

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan meyer:sync-tracking`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/meyer-sync-tracking/meyer-sync-tracking-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each update logs: meyer_order_id, tracking_number, carrier, item_id
- [ ] Summary: total checked, tracking received, still pending, errors
- [ ] Sets OrdersItems.current_status=6, tracking, carrier correctly

## Monitoring
- Log: `storage/logs/meyer-sync-tracking/`
- Health: `SELECT COUNT(*) FROM meyer_orders WHERE processed = 1` — should decrease as tracking arrives
- Post-cutover: verify Meyer orders get tracking within expected timeframe

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/MeyerTask.php` lines 150-196

Key logic:
1. Find meyer_orders WHERE processed=1
2. For each: call MeyerApiHelper::processTracking(meyer_order_id)
3. If API returns tracking: update OrdersItems.tracking, current_status=6, carrier=ShipMethod
4. Set meyer_orders.processed=2

## Create: app/Console/Commands/MeyerSyncTracking.php
Signature: `meyer:sync-tracking {--dry-run}`

1. Port processTrackingAction() logic
2. Use existing MeyerOrder and OrderItem models
3. Status 6 = Shipped (verify with ItemsStatus model)
4. Schedule: everyFifteenMinutes()->withoutOverlapping()
5. Log channel: 'meyer-sync-tracking'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# KEYSTONE/LKQ
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-17: Keystone/LKQ Tracking Import",
    "tags": ["keystone", "lkq", "phase-2", "tracking"],
    "description": """## What
Migrate Keystone/LKQ tracking import from old Central. Reads ZIP files containing XML tracking data from Keystone, parses them, and updates order item tracking info.

**Old cron:** `*/15 * * * * php cli.php Lkqtracking processTracking`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/LkqtrackingTask.php` → `processTrackingAction()` (lines 35-end)
- Reads ZIP files from `/home/goparts/go-parts.com/keystone/`
- Extracts ZIPs, parses XML files inside
- Each XML has: PONumber, LineItemCount, Product elements with StockNumber + TrackingNumber
- Matches PONumber → Orders.external_order_id → OrdersItems by partslink
- Updates OrdersItems.tracking and current_status=6
- Moves processed ZIPs to `processed/` subdirectory

### New System Files to Create
- `app/Console/Commands/KeystoneImportTracking.php`

## Release Batch
**Keystone/LKQ** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan keystone:import-tracking`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Reads from `/home/goparts/go-parts.com/keystone/` (verify path access)
- [ ] Processes ZIP → XML → extract tracking per order
- [ ] Handles single-item and multi-item orders correctly
- [ ] Moves processed ZIPs to `processed/` subdirectory
- [ ] Logs to `storage/logs/keystone-tracking/keystone-tracking-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each processed file logs: ZIP name, XML count, orders updated, tracking numbers found
- [ ] Summary: ZIPs processed, orders updated, items updated, errors

## Monitoring
- Log: `storage/logs/keystone-tracking/`
- Health: check `/home/goparts/go-parts.com/keystone/` for unprocessed ZIPs growing — indicates failures
- Post-cutover: verify Keystone/LKQ orders receive tracking

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/LkqtrackingTask.php` lines 35-end (full processTrackingAction)

Key logic:
1. glob('/home/goparts/go-parts.com/keystone/*.zip'), sort by mtime ascending
2. For each ZIP: extract to temp dir, parse XML files
3. XML structure: PONumber, LineItemCount, LineItems->Product->StockNumber/TrackingNumber
4. Handle single vs multi item orders differently
5. Match: Orders.external_order_id = PONumber, then find OrdersItems by partslink match
6. Update: OrdersItems.tracking = TrackingNumber, current_status = 6
7. Move ZIP to processed/ directory

## Create: app/Console/Commands/KeystoneImportTracking.php
Signature: `keystone:import-tracking {--dry-run}`

1. Port the full processTrackingAction() logic
2. Use PHP ZipArchive + simplexml_load_file (same as old code)
3. Ensure path /home/goparts/go-parts.com/keystone/ is readable by ea-php83
4. Schedule: everyFifteenMinutes()->withoutOverlapping()
5. Log channel: 'keystone-tracking'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# AMAZON
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-18: Amazon Tracking Updates",
    "tags": ["amazon", "phase-2", "tracking"],
    "description": """## What
Migrate Amazon MCF (Multi-Channel Fulfillment) tracking updates from old Central. Polls Amazon SP-API for fulfillment order statuses and tracking, creates dropshipper_tracking records, and handles unfulfillable orders.

**Old cron:** `*/10 * * * * php cli.php amazon updateTrackings`
**Frequency:** Every 10 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/AmazonTask.php` → `updateTrackingsAction()` (lines 100-146)
- `_processOrderTrackings()` (lines 299-376) — handles SHIPPED status: creates DropshipperTracking records (supplier_id=47)
- `_processOrderUnfulfillable()` (lines 383-443) — handles UNFULFILLABLE: reverts items to STATUS_NEEDS_ORDER, adds notes
- `/home/opsgoparts/www/central/app/helpers/AmazonShippingHelper.php` — SP-API client
- Processes statuses: COMPLETE, COMPLETE_PARTIALLED, PROCESSING → check tracking; UNFULFILLABLE → revert
- Carrier "AMAZON LOGISTICS" mapped to "Amazon Shipping"

### New System Files to Create
- `app/Console/Commands/AmazonSyncTracking.php`
- May need Amazon SP-API library

## Release Batch
**Amazon** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan amazon:sync-tracking`
- [ ] `--dry-run` flag, `--days=N` (default 7)
- [ ] Registered in Kernel.php: `everyTenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/amazon-sync-tracking/amazon-sync-tracking-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Handles SHIPPED: creates DropshipperTracking (supplier_id=47), maps Amazon Logistics carrier
- [ ] Handles CANCELLED_BY_FULFILLER: reverts item to Needs Order, adds note
- [ ] Handles UNFULFILLABLE: reverts all items, adds note
- [ ] Skips already-existing tracking records (dedup by external_order_id + tracking_number)
- [ ] Summary: orders checked, tracking created, unfulfillable handled, errors

## Monitoring
- Log: `storage/logs/amazon-sync-tracking/`
- Health: verify Amazon MCF orders are getting tracking within expected timeframe
- Alert: growing UNFULFILLABLE count may indicate Amazon inventory issues
- Post-cutover: compare DropshipperTracking records for supplier_id=47

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/AmazonTask.php`:
- updateTrackingsAction() lines 100-146
- _processOrderTrackings() lines 299-376
- _processOrderUnfulfillable() lines 383-443

`/home/opsgoparts/www/central/app/helpers/AmazonShippingHelper.php`:
- listAllFulfillmentOrders($days) — returns [orderId => status]
- getFulfillmentOrder($order) — returns shipments/items details

Key logic:
1. List all fulfillment orders from last N days
2. For COMPLETE/PROCESSING: get tracking, create DropshipperTracking (supplier_id=47)
3. "AMAZON LOGISTICS" carrier → map to "Amazon Shipping"
4. For CANCELLED_BY_FULFILLER items: revert to STATUS_NEEDS_ORDER, add note
5. For UNFULFILLABLE orders: revert all items, add note

## Create: app/Console/Commands/AmazonSyncTracking.php
Signature: `amazon:sync-tracking {--days=7} {--dry-run}`

1. Port all three methods' logic
2. Port or reuse AmazonShippingHelper (SP-API client)
3. Create DropshipperTracking model if not exists
4. Schedule: everyTenMinutes()->withoutOverlapping()
5. Log channel: 'amazon-sync-tracking'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# RPMWARE
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-19: RPMWare Order Import",
    "tags": ["rpmware", "phase-2", "orders"],
    "description": """## What
Migrate RPMWare order import from old Central. Imports orders from rpmware.com for two accounts: HIN and Racing.

**Old cron:** `42 */4 * * * php cli.php rpmware import`
**Frequency:** Every 4 hours (at :42)

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/RpmwareTask.php` → `importAction()` (lines 15-27)
- `/home/opsgoparts/www/central/app/helpers/RpmWareHelper.php` → `loadConfig()`, `importOrders()`
- Two accounts: 'hin' and 'racing' — each has separate config
- Imports orders from RPMWare API into Central's orders system

### New System Files to Create
- `app/Console/Commands/RpmwareImportOrders.php`

## Release Batch
**RPMWare** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan rpmware:import-orders`
- [ ] `--dry-run` flag, `--account=` option (hin, racing, or all)
- [ ] Registered in Kernel.php: `cron('42 */4 * * *')->withoutOverlapping()`
- [ ] Imports from both HIN and Racing accounts
- [ ] Logs to `storage/logs/rpmware-import/rpmware-import-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each import logs: account, orders found, orders imported, skipped (duplicates), errors
- [ ] Summary: total imported per account

## Monitoring
- Log: `storage/logs/rpmware-import/`
- Health: if 0 orders imported for 24+ hours, verify RPMWare API connectivity
- Post-cutover: compare order counts from RPMWare between old and new

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/RpmwareTask.php` lines 15-27
- Trace into RpmWareHelper::loadConfig('hin'), RpmWareHelper::importOrders()
- Then RpmWareHelper::loadConfig('racing'), importOrders() again

## Create: app/Console/Commands/RpmwareImportOrders.php
Signature: `rpmware:import-orders {--account=all : hin, racing, or all} {--dry-run}`

1. Port RpmWareHelper logic for both accounts
2. Config: check where RPMWare API credentials are stored (old config files)
3. Schedule: cron('42 */4 * * *')->withoutOverlapping()
4. Log channel: 'rpmware-import'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# SUPPLIER IMPORTS (each separate)
# ═══════════════════════════════════════════════════════════════════════════════

supplier_imports = [
    {"code": "maxzone", "name": "Maxzone", "schedule": "cron('14 */4 * * *')", "helper": "MaxzoneImportHelper", "method": "processEmailImport", "source": "email"},
    {"code": "elt", "name": "ELT", "schedule": "cron('21 */4 * * *')", "helper": "ELTImportHelper", "method": "processEmailImport", "source": "email"},
    {"code": "dtl", "name": "DTL", "schedule": "cron('28 */4 * * *')", "helper": "DTLImportHelper", "method": "processEmailImport", "source": "email"},
    {"code": "expressparts", "name": "ExpressParts", "schedule": "dailyAt('01:00')", "helper": "ExpresspartImportHelper", "method": "processEmailImport", "source": "email"},
    {"code": "perfradiator", "name": "PerfRadiator", "schedule": "cron('0 */4 * * *')", "helper": "PerfradiatorImportHelper", "method": "processFileImport", "source": "file"},
    {"code": "pbi", "name": "PBI", "schedule": "cron('49 */4 * * *')", "helper": "PBIImportHelper", "method": "processEmailImport", "source": "email"},
    {"code": "meyer-catalog", "name": "Meyer Catalog", "schedule": "dailyAt('02:32')", "helper": "MeyerImportHelper", "method": "processFileImport", "source": "file"},
]

for i, sup in enumerate(supplier_imports):
    task_num = 20 + i
    TASKS.append({
        "name": f"PH2-{task_num}: Supplier Import — {sup['name']}",
        "tags": ["supplier-import", "phase-2", "catalog"],
        "description": f"""## What
Migrate {sup['name']} supplier price/inventory import from old Central. Imports data from {sup['source']} and updates `parts_suppliers` table.

**Old cron:** `php cli.php partslink {sup['code']}Import`
**Schedule:** `{sup['schedule']}`

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php` → `{sup['code']}ImportAction()`
- `/home/opsgoparts/www/central/app/helpers/Supplier/{sup['helper']}.php` → `{sup['method']}()`
- Source: {'reads import data from email inbox' if sup['source'] == 'email' else 'reads import data from file drop'}

### New System Files to Create
- `app/Console/Commands/SupplierImport{sup['name'].replace(' ', '')}.php`

## Release Batch
**Supplier Imports** — release all 7 supplier imports together

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan supplier:import-{sup['code']}`
- [ ] `--dry-run` flag, `--debug` flag for verbose output
- [ ] `--file=` option to process a specific file instead of {sup['source']}
- [ ] Registered in Kernel.php: `{sup['schedule']}->withoutOverlapping()`
- [ ] Logs to `storage/logs/supplier-import-{sup['code']}/supplier-import-{sup['code']}-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: records processed, created, updated, errors
- [ ] parts_suppliers data matches old system for same input

## Monitoring
- Log: `storage/logs/supplier-import-{sup['code']}/`
- Health: if no import for 24+ hours, check {sup['source']} source availability
- Compare: parts_suppliers record counts and prices between old and new

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + f"""
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php` → `{sup['code']}ImportAction()`
- `/home/opsgoparts/www/central/app/helpers/Supplier/{sup['helper']}.php` → `{sup['method']}()`

## Create: app/Console/Commands/SupplierImport{sup['name'].replace(' ', '')}.php
Signature: `supplier:import-{sup['code']} {{--file= : Specific file path}} {{--debug}} {{--dry-run}}`

1. Port {sup['helper']}::{sup['method']}() logic
2. {'Port email reading logic (IMAP connection)' if sup['source'] == 'email' else 'Port file reading logic'}
3. Schedule: {sup['schedule']}->withoutOverlapping()
4. Log channel: 'supplier-import-{sup['code']}'
"""
    })

# ═══════════════════════════════════════════════════════════════════════════════
# PARTSLINK CORE
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-27: Partslink Sync & Processing",
    "tags": ["partslink", "phase-2", "catalog"],
    "description": """## What
Migrate the partslink.sh pipeline from old Central. Runs 4 sequential operations: reindex discounted parts, update products by partslink, build partslink list, update entity links.

**Old cron:** `*/15 * * * * /home/opsgoparts/public_html/central/shell/partslink.sh`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/public_html/central/shell/partslink.sh` runs these in sequence:
  1. `partslink discountedParts` → `PartsSuppliersHelper::reindexDiscountedParts()`
  2. `partslink main` → `PriceImport::updateProductsByPartslink()` (1000 records per run from partslink_cron table)
  3. `partslink buildList` → `PartslinkHelper::buildList()`
  4. `partslink entityListLink` → `PartslinkHelper::updateAllEntities()`
- `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php` — all 4 actions
- Heavy memory usage: 3048M limit

### New System Files to Create
- `app/Console/Commands/PartslinkSync.php` — combines all 4 steps

## Release Batch
**Partslink Core** — release with PH2-28

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan partslink:sync`
- [ ] `--dry-run` flag, `--step=` option to run individual steps (discounted, main, build-list, entity-link)
- [ ] `--limit=N` for the main step (default 1000)
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/partslink-sync/partslink-sync-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Each step logs: step name, records processed, duration
- [ ] Summary: all steps completed, total time

## Monitoring
- Log: `storage/logs/partslink-sync/`
- Health: check partslink_cron table — records should be processed regularly (updated_at advancing)
- Post-cutover: product prices should continue updating as expected

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
Shell script: `/home/opsgoparts/public_html/central/shell/partslink.sh`
Runs these 4 commands in sequence:
1. `PartslinkTask::discountedPartsAction()` → PartsSuppliersHelper::reindexDiscountedParts()
2. `PartslinkTask::mainAction()` → PriceImport::updateProductsByPartslink() — 1000 records from partslink_cron
3. `PartslinkTask::buildListAction()` → PartslinkHelper::buildList()
4. `PartslinkTask::entityListLinkAction()` → PartslinkHelper::updateAllEntities()

Task file: `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php`

## Create: app/Console/Commands/PartslinkSync.php
Signature: `partslink:sync {--step= : Run specific step only} {--limit=1000} {--dry-run}`

1. Run all 4 steps in sequence, or a single step with --step
2. Port each helper's logic (PartsSuppliersHelper, PriceImport, PartslinkHelper)
3. Memory: set_time_limit(0), memory_limit as needed
4. Schedule: everyFifteenMinutes()->withoutOverlapping()
5. Log channel: 'partslink-sync'
"""
})

TASKS.append({
    "name": "PH2-28: Partslink Discontinued Parts OOS Marking",
    "tags": ["partslink", "phase-2", "catalog"],
    "description": """## What
Migrate the discontinued parts OOS (Out of Stock) marking from old Central. Marks old discontinued parts as out-of-stock in parts_suppliers.

**Old cron:** `0 6 * * * php cli.php partslink updateDiscontinuedPartsOutOfStock`
**Frequency:** Daily at 6:00 AM

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php` → `updateDiscontinuedPartsOutOfStockAction()` (lines 557-561)
- `/home/opsgoparts/www/central/app/helpers/PartsSuppliersHelper.php` → `updateDiscontinuedPartsOutOfStock()`

### New System Files to Create
- `app/Console/Commands/PartslinkMarkDiscontinued.php`

## Release Batch
**Partslink Core** — release with PH2-27

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan partslink:mark-discontinued`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `dailyAt('06:00')`
- [ ] Logs to `storage/logs/partslink-discontinued/partslink-discontinued-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: total checked, marked OOS, already OOS, errors

## Monitoring
- Log: `storage/logs/partslink-discontinued/`
- Verify: discontinued parts are correctly marked as out-of-stock
- Post-cutover: compare OOS counts before/after

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PartslinkTask.php` lines 557-561
- Trace into PartsSuppliersHelper::updateDiscontinuedPartsOutOfStock()

## Create: app/Console/Commands/PartslinkMarkDiscontinued.php
Signature: `partslink:mark-discontinued {--dry-run}`

1. Port PartsSuppliersHelper::updateDiscontinuedPartsOutOfStock() logic
2. Schedule: dailyAt('06:00')
3. Log channel: 'partslink-discontinued'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# WHIMS
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-29: WHIMS Inventory Sync",
    "tags": ["whims", "phase-2", "inventory"],
    "description": """## What
Migrate WHIMS inventory sync from old Central. Syncs warehouse inventory from WHIMS system to `partscan_inventory` (or `partscan_inventory_whims`) table. This replaced the older Finale inventory sync.

**Old cron:** `5 * * * * php cli.php whimsinventory syncStocks`
**Frequency:** Hourly (at :05)

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/WhimsinventoryTask.php` → `syncStocksAction()` (lines 23-65)
- `/home/opsgoparts/www/central/app/models/WhimsInventoryImportModel.php` → `importProductsWithStocks()`
- Returns stats: total, created, updated, errors
- Memory-intensive: 4096M limit

### New System Files to Create
- `app/Console/Commands/WhimsSyncInventory.php`

## Release Batch
**WHIMS** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan whims:sync-inventory`
- [ ] `--dry-run` flag, `--debug` flag
- [ ] Registered in Kernel.php: `hourlyAt(5)->withoutOverlapping()`
- [ ] Logs to `storage/logs/whims-sync-inventory/whims-sync-inventory-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: total processed, created, updated, errors, execution time
- [ ] Memory limit configured appropriately for large dataset

## Monitoring
- Log: `storage/logs/whims-sync-inventory/`
- Health: if sync fails or 0 records for 4+ hours, WHIMS connection may be down
- Compare: inventory counts between old and new during parallel run
- Post-cutover: verify inventory levels are current (compare with WHIMS source)

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/WhimsinventoryTask.php` lines 23-65
- Trace into WhimsInventoryImportModel::importProductsWithStocks()
- Understand: where does WHIMS data come from? (API? shared DB? file?)

## Create: app/Console/Commands/WhimsSyncInventory.php
Signature: `whims:sync-inventory {--debug} {--dry-run}`

1. Port WhimsInventoryImportModel::importProductsWithStocks() logic
2. Handle memory: ini_set('memory_limit', '4096M') or chunk processing
3. Schedule: hourlyAt(5)->withoutOverlapping()
4. Log channel: 'whims-sync-inventory'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# PRICE
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-30: Schedule Price Updates",
    "tags": ["price", "phase-2", "catalog"],
    "description": """## What
Migrate the price update scheduler from old Central. Triggers price updates based on default configuration — updates product prices from parts_suppliers data.

**Old cron:** `30 4 * * * php cli.php price scheduleUpdatePriceTask`
**Frequency:** Daily at 4:30 AM

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/PriceTask.php` → `scheduleUpdatePriceTaskAction()` (lines 133-137)
- `/home/opsgoparts/www/central/app/models/PriceImportModel.php` → `updatePriceByDefaultConfig()`
- Recalculates product prices based on supplier pricing rules and configuration

### New System Files to Create
- `app/Console/Commands/SchedulePriceUpdates.php`

## Release Batch
**Price** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan price:schedule-update`
- [ ] `--dry-run` flag
- [ ] Registered in Kernel.php: `dailyAt('04:30')->withoutOverlapping()`
- [ ] Logs to `storage/logs/price-schedule-update/price-schedule-update-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary: products repriced, unchanged, errors

## Monitoring
- Log: `storage/logs/price-schedule-update/`
- Verify: product prices are updated daily
- Post-cutover: spot-check 20 product prices against parts_suppliers source data

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/PriceTask.php` lines 133-137
- Trace into PriceImportModel::updatePriceByDefaultConfig()

## Create: app/Console/Commands/SchedulePriceUpdates.php
Signature: `price:schedule-update {--dry-run}`

1. Port PriceImportModel::updatePriceByDefaultConfig() logic
2. Schedule: dailyAt('04:30')->withoutOverlapping()
3. Log channel: 'price-schedule-update'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# ITEM SUPPLIERS
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-31: Item-Supplier Mapping Updates",
    "tags": ["item-suppliers", "phase-2", "catalog"],
    "description": """## What
Migrate item-supplier mapping updates from old Central. For shipped orders (last 8 days), records which supplier fulfilled each item and whether it was the cheapest option. Populates the `item_suppliers` table.

**Old cron:** `*/15 * * * * php cli.php Itemsuppliers update`
**Frequency:** Every 15 minutes

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/ItemsuppliersTask.php` → `updateAction()` (lines 31-171)
- Complex logic:
  1. Finds orders from last 8 days with shipped items (current_status=6)
  2. For each item: looks up parts_suppliers to find buy price (price + shipping + handling)
  3. USAuto B2C: price + shipping only (no handling)
  4. Gets all available suppliers and their prices
  5. Determines cheapest supplier
  6. Inserts into `item_suppliers` if not already exists
- Uses OrdersProfiler, SuppliersHelper, PartsSuppliersHelper

### New System Files to Create
- `app/Console/Commands/UpdateItemSuppliers.php`

## Release Batch
**Item Suppliers** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan item-suppliers:update`
- [ ] `--dry-run` flag, `--days=N` (default 8)
- [ ] Registered in Kernel.php: `everyFifteenMinutes()->withoutOverlapping()`
- [ ] Logs to `storage/logs/item-suppliers-update/item-suppliers-update-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Correctly calculates buy price (USAuto B2C: price+shipping; all others: price+shipping+handling)
- [ ] Identifies cheapest supplier accurately
- [ ] Skips already-recorded items
- [ ] Summary: orders checked, items recorded, skipped (existing), errors

## Monitoring
- Log: `storage/logs/item-suppliers-update/`
- Health: `SELECT COUNT(*) FROM item_suppliers WHERE created_at > NOW() - INTERVAL 1 DAY` should be > 0 on active days
- Post-cutover: compare item_suppliers records between old and new

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
`/home/opsgoparts/www/central/app/tasks/ItemsuppliersTask.php` lines 31-171 (full updateAction)

Key logic:
1. Query orders placed in last 8 days with items at current_status=6 (Shipped)
2. For each shipped item: get partslink, look up parts_suppliers for buy price
3. USAuto B2C (source != 500): price + shipping (no handling)
4. All others: price + shipping + handling
5. Get all available suppliers via OrdersItemsHelper::getPartsSuppliersHtml()
6. Find cheapest supplier
7. Insert into item_suppliers table if not exists

## Create: app/Console/Commands/UpdateItemSuppliers.php
Signature: `item-suppliers:update {--days=8} {--dry-run}`

1. Port the full updateAction() logic
2. Use existing models: Order, OrderItem, PartsSupplier, Supplier
3. Create ItemSupplier model if not exists (table: item_suppliers)
4. Port price calculation logic exactly (USAuto B2C exception)
5. Schedule: everyFifteenMinutes()->withoutOverlapping()
6. Log channel: 'item-suppliers-update'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# MAGENTO
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-32: Magento URL Key Sync — Evaluate Need",
    "tags": ["magento", "phase-2", "evaluation"],
    "description": """## What
**EVALUATION TASK** — Determine if the Magento URL key sync is still needed. If Magento is being phased out, this may be obsolete.

**Old cron:** `12 1 * * * php cli.php products syncMagentoUrlKeysToGoparts`
**Frequency:** Daily at 1:12 AM

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/ProductsTask.php` → `syncMagentoUrlKeysToGopartsAction()` (lines 34-40)
- `/home/opsgoparts/www/central/app/helpers/ProductHelper.php` → `syncMagentoUrlKeysToGoparts()`
- Syncs URL keys from Magento products to goparts products table

### Key Questions
1. Is Magento still the active storefront?
2. Is the go-parts.com website still using Magento URL keys for product pages?
3. If Magento is being replaced, is this sync needed during transition period?

## Release Batch
**Magento** — standalone, may be SKIP

## Acceptance Criteria
- [ ] Decision documented: NEEDED, TEMPORARY (until Magento sunset), or SKIP
- [ ] If NEEDED: create `ea-php83 artisan magento:sync-url-keys` command
- [ ] If TEMPORARY: create command with a sunset date noted
- [ ] If SKIP: document why

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Evaluate
1. Read: `/home/opsgoparts/www/central/app/tasks/ProductsTask.php` lines 34-40
2. Trace into ProductHelper::syncMagentoUrlKeysToGoparts()
3. Understand: what table does it write to? Is that table still used by the live website?
4. Check: is there a Magento database connection configured in new Central?
5. Decision: is this still needed?
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# REPORTING
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-33: Non-Selling Items Report",
    "tags": ["reporting", "phase-2", "analytics"],
    "description": """## What
Migrate the non-selling items report from old Central. Identifies products that haven't sold within various time windows (180, 365, 547, 730, 1095 days).

**Old cron:** `30 0 * * * php cli.php ordersitems buildNonSellingItems`
**Frequency:** Daily at 12:30 AM

## Scope
### Old System Files
- `/home/opsgoparts/www/central/app/tasks/OrdersitemsTask.php` → `buildNonSellingItemsAction()` (lines 16-28)
- `/home/opsgoparts/www/central/app/helpers/OrdersItemsHelper.php` → `buildNonSellingItems($days)`
- Runs 5 times with different thresholds: 180, 365, 547, 730, 1095 days

### New System Files to Create
- `app/Console/Commands/BuildNonSellingItems.php`

## Release Batch
**Reporting** — standalone release

## Acceptance Criteria
- [ ] Artisan command: `ea-php83 artisan reporting:non-selling-items`
- [ ] `--dry-run` flag, `--days=N` to run specific threshold only
- [ ] Registered in Kernel.php: `dailyAt('00:30')`
- [ ] Processes all 5 thresholds: 180, 365, 547, 730, 1095 days
- [ ] Logs to `storage/logs/non-selling-items/non-selling-items-YYYY-MM-DD.log`
- [ ] Log rotation: daily, 14-day retention
- [ ] Summary per threshold: items identified, new entries, total in list

## Monitoring
- Log: `storage/logs/non-selling-items/`
- Verify: non-selling items table is populated daily
- Post-cutover: compare counts per threshold between old and new

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Old Code Reference
- `/home/opsgoparts/www/central/app/tasks/OrdersitemsTask.php` lines 16-28
- Trace into OrdersItemsHelper::buildNonSellingItems($days)
- Called 5 times: 180, 365, 547, 730, 1095

## Create: app/Console/Commands/BuildNonSellingItems.php
Signature: `reporting:non-selling-items {--days= : Run specific threshold only} {--dry-run}`

1. Port OrdersItemsHelper::buildNonSellingItems() logic
2. Run for all 5 thresholds by default, or single with --days
3. Schedule: dailyAt('00:30')
4. Log channel: 'non-selling-items'
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# GOPARTS ORDERING SUB-APP
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-34: GoParts Ordering Sub-App — Evaluate & Migrate",
    "tags": ["goparts-ordering", "phase-2", "evaluation"],
    "description": """## What
**EVALUATION + MIGRATION** — The old Central hosts a separate Laravel sub-app at `goparts-ordering/AutoParts/` that has its own scheduler with 2 tasks. Decide whether to merge into new Central or keep separate.

**Old cron:** `* * * * * php goparts-ordering/AutoParts/artisan schedule:run`
**Contains:**
1. `Turn14Controller@stockExport` — daily at 1:10 AM PST (stock export to Turn14)
2. `OrderController@processOrders` — every 5 minutes (processes incoming orders)

## Scope
### Old System Files
- `/home/opsgoparts/public_html/central/public/goparts-ordering/AutoParts/app/Console/Kernel.php`
- `Turn14Controller@stockExport` — called via `$schedule->call()`
- `OrderController@processOrders` — called via `$schedule->call()`, every 5 min, withoutOverlapping

### Key Questions
1. Is goparts-ordering still actively receiving orders?
2. Is Turn14 still an active supplier requiring stock exports?
3. Should these be absorbed into new Central as artisan commands?
4. Or should goparts-ordering remain as a separate app with its own cron?

## Release Batch
**GoParts Ordering** — standalone, depends on evaluation

## Acceptance Criteria
- [ ] Decision: MERGE into new Central, KEEP separate, or DECOMMISSION
- [ ] If MERGE: create `turn14:stock-export` and `ordering:process-orders` commands
- [ ] If KEEP: document the separate cron and ensure it runs under correct user
- [ ] If DECOMMISSION: verify no active orders/exports, disable cron

## Dev Prompt (copy-paste to Claude)

""" + ENV_BLOCK + """
## Evaluate
1. Read: `/home/opsgoparts/public_html/central/public/goparts-ordering/AutoParts/app/Console/Kernel.php`
2. Read: Turn14Controller@stockExport and OrderController@processOrders
3. Check DB: are there recent orders in the goparts_ordering database?
   `SELECT COUNT(*) FROM goparts_ordering.orders WHERE created_at > NOW() - INTERVAL 7 DAY`
4. Decision: merge, keep, or decommission?
"""
})

# ═══════════════════════════════════════════════════════════════════════════════
# CUTOVER
# ═══════════════════════════════════════════════════════════════════════════════

TASKS.append({
    "name": "PH2-35: Parallel Run Verification",
    "tags": ["cutover", "phase-2"],
    "description": """## What
Run both old and new Central backend tasks in parallel for 1 week. Verify that new commands produce identical results to old cron jobs.

## Scope
### Verification Checklist (per task)
For each migrated task, during the parallel run week:
1. Both old cron and new scheduled command are active
2. Compare DB state changes: same records updated, same values
3. Compare log output: same orders/items processed
4. No duplicate processing (both systems shouldn't create duplicate records)
5. Performance: new commands complete within similar timeframes

### Potential Issues
- **Duplicate records**: Both systems may try to insert the same record. Ensure commands use "insert if not exists" / upsert patterns
- **Race conditions**: Old and new may process the same order simultaneously
- **Lock conflicts**: withoutOverlapping() on new side, file locks on old side

### Mitigation
- Enable --dry-run on new commands initially, compare logs only
- Then enable writes but with dedup protection
- Finally disable old cron entries one batch at a time

## Release Batch
**Cutover** — after ALL migration tasks are complete

## Acceptance Criteria
- [ ] All migrated commands running in parallel for 7 days
- [ ] Daily comparison report: records processed by old vs new
- [ ] Zero discrepancies in DB state
- [ ] No duplicate records created
- [ ] No performance degradation
- [ ] Sign-off from operations team

## Monitoring
- Daily comparison queries for each task
- Error logs reviewed daily
- Operations team validates order processing is normal
"""
})

TASKS.append({
    "name": "PH2-36: Final Decommission & Archive",
    "tags": ["cutover", "phase-2"],
    "description": """## What
After successful parallel run, disable all old Central cron jobs and archive the old codebase.

## Scope
### Steps
1. **Comment out** all entries in `opsgoparts` crontab (don't delete — comment with date)
2. Verify new Central tasks are running correctly for 48h with old cron disabled
3. Archive old Central codebase: `tar -czf /home/opsgoparts/archive/central-$(date +%Y%m%d).tar.gz /home/opsgoparts/www/central/`
4. Update documentation: mark Phase II as complete
5. Clean up any old Central temp files, logs, sessions

### Do NOT
- Delete the old codebase until 30 days after cutover
- Remove the `opsgoparts` user account
- Drop any database tables (old and new share the same DB)

## Release Batch
**Cutover** — final step, after PH2-35 is verified

## Acceptance Criteria
- [ ] All `opsgoparts` crontab entries commented out with date stamp
- [ ] New Central running all tasks independently for 48+ hours
- [ ] Old Central codebase archived to tar.gz
- [ ] Operations team confirms no issues for 1 week
- [ ] Phase II marked complete in ClickUp
- [ ] CLAUDE.md / project memory updated

## Monitoring
- Watch all new Central logs for 1 week post-cutover
- Operations team reports any anomalies
- Keep old archive for 30 days minimum
"""
})


# ─── Create tasks via API ────────────────────────────────────────────────────

def create_task(task_data):
    """Create a single task in ClickUp."""
    url = f"{BASE_URL}/list/{LIST_ID}/task"

    payload = {
        "name": task_data["name"],
        "markdown_description": task_data["description"],
        "status": "backlog",
        "tags": task_data.get("tags", []),
    }

    data = json.dumps(payload).encode('utf-8')
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": API_KEY,
            "Content-Type": "application/json",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode())
            return True, result.get("id"), result.get("name")
    except urllib.error.HTTPError as e:
        error_body = e.read().decode()
        return False, None, f"HTTP {e.code}: {error_body[:200]}"
    except Exception as e:
        return False, None, str(e)


def main():
    print(f"Creating {len(TASKS)} tasks in ClickUp list {LIST_ID}...")
    print("=" * 60)

    success_count = 0
    fail_count = 0

    for i, task in enumerate(TASKS):
        success, task_id, info = create_task(task)

        if success:
            print(f"[{i+1:2d}/{len(TASKS)}] OK  {task_id}: {info}")
            success_count += 1
        else:
            print(f"[{i+1:2d}/{len(TASKS)}] ERR {task['name']}: {info}")
            fail_count += 1

        # Small delay to avoid rate limiting
        time.sleep(0.3)

    print("=" * 60)
    print(f"Done: {success_count} created, {fail_count} failed")


if __name__ == "__main__":
    main()
