# WP-IMG-2 — Import 1000 products from the moonui catalog, images through the CORRECT path

**Repo:** BE only — `/home/moonui4/moon-erp-be` (branch `hazemdev4`)
**Depends on:** WP-IMG-1 (image URL fixes) — **must be ✅ and merged into the working tree first**
**Review:** code-reviewer · **[FIN]:** no · **Migration:** **NO**
**Blocked until:** the owner runs the export command; input lands at `/home/moonui4/import/`

## Why this exists

The moonui (moon 1) instance holds ~17k products whose images were pushed through the **generic attachments API** — a private-disk, internal-documents system with no notion of "main image" or ordering. They 404 for any public storefront. The owner confirmed **no real customer ever used that data**, so the fix is to **re-import it through the correct path** rather than bridge over the mistake.

Moon ERP already has the right system, and it is completely unused (`product_images` had **0 rows**):

| Field | Meaning — this IS the "marker" |
|---|---|
| `products.image` | the **main** image (a path on the **public** disk) |
| `product_images.image` + `.position` | the **gallery**, ordered ascending |

This is exactly what a manual upload from the Moon product screen produces (`Modules/Core/app/Http/Controllers/ProductController.php:92-108` via `HandlesImageUpload::resolveImageField`, which stores to disk `public`). **The import must produce a byte-for-byte equivalent end state** — a product imported by this WP must be indistinguishable from one uploaded by hand.

## Source — already surveyed by the orchestrator (2026-07-19), verified counts

**Source DB: `moonui_dev_be`** — reachable directly via the local `mysql` client as root. **READ-ONLY. `SELECT` only. ⛔ Never write, alter, or drop anything in it.** No dump/scratch DB is needed; query it directly.

**Source files:** `/home/moonui/moon-erp-be/storage/app/private/attachments/4/2026-03/…` — readable with `cp` (verified: one file copied, `JPEG 800x800`). **⛔ Read-only. Never write, move, or delete anything under `/home/moonui`.**

Verified figures — do not re-derive them, but DO report if reality differs:

| Fact | Value |
|---|---|
| `products` | **17,156** |
| products with a non-empty `products.image` | **14** ← the correct column is essentially unused |
| `product_images` rows | **8** |
| `attachments` rows | **17,149** |
| of those: `attachable_type = Modules\Core\Models\Product`, `mime_type = image/jpeg` | **17,136** |
| products with exactly 1 image / exactly 2 images | **17,116 / 10** |
| `product_categories` | **690** |
| total image bytes | **0.61 GB** (so ~36 MB for a 1000-product sample) |

`file_path` values look like `attachments/4/2026-03/<40-char>.jpg` — already unique 40-char random names, so collision risk is low, **but still namespace them on import** (see below) because the target folder is shared with other sources.

### 🔴 The single most dangerous fact

**Every source row is `company_id = 4`. The storefront on this install serves `company_id = 1`.**

If the import carries `company_id` across verbatim, **every imported row will be invisible** — the storefront will show nothing, and **nothing will error**, because the backend has **zero Eloquent global scopes** to catch it. You must map **4 → 1** explicitly on every inserted row (products, categories, product_images) and assert it afterwards.

Owner decision (2026-07-19): this data may be published on the public storefront — no anonymisation of names or prices is required.

## The job

Import **1000 products** (not 17k — the owner chose a sample first) into `moonui4_dev_be`, `company_id = 1`, with:

1. **Category tree first**, so products land in real categories rather than all in one bucket. Preserve parent/child structure and both `name` / `name_ar`.
2. **Files copied to `storage/app/public/uploads/products/`** with **unique names** — derive them from a hash or the source id. ⛔ **Never `basename()`**: two companies' files can share a name and one would silently overwrite the other, showing company A's photo on company B's product. This is a real leak, not a theoretical one.
3. **Image assignment:**
   - only attachments whose `mime_type LIKE 'image/%'` are eligible — a PDF datasheet must never become a product photo;
   - the **first eligible image (oldest `id`)** → `products.image`;
   - **every remaining eligible image** → a `product_images` row with `position` ascending from 0;
   - a product with no eligible image gets `image = null` (the storefront shows its placeholder correctly — do not invent a filler).
4. **Bilingual data** — `name`/`name_ar` both populated wherever the source has them. Sale price, barcode, SKU, category link.

## Constraints — absolute

1. ⛔ **NEVER** `migrate:fresh`/`migrate:refresh`/`db:wipe`/`RefreshDatabase`. `moonui4_dev_be` is **not binlogged** — a wrong wipe is unrecoverable. If you think you need one, STOP and report.
2. ⛔ **`/home/moonui` and `moonui_dev_be` are READ-ONLY.** `SELECT` and `cp` out only. Never write, alter, drop, move or delete anything there. `/home/moonui2` and `/home/moonui3` are off-limits entirely.
2b. ⛔ **Map `company_id` 4 → 1 on every inserted row** and assert `COUNT(*) WHERE company_id != 1 = 0` afterwards. Getting this wrong silently imports invisible data.
3. ⛔ **Never delete, truncate or modify rows you did not create.** The 24 existing demo products stay exactly as they are — the owner uses them. Create only.
4. **Idempotent.** Running 3× must leave identical counts. Key on a stable natural key (source id or SKU) via `withTrashed()->firstOrNew()` — these models extend `BaseModel` with `SoftDeletes`, so a plain `updateOrCreate` cannot see a trashed row while the unique index still counts it. This bit an earlier WP on this project.
5. Every query carries its own `company_id` filter — the backend has **zero Eloquent global scopes**.
6. **No migration.** The columns exist.
7. Human-invoked only — not wired into `DatabaseSeeder`, `local-deploy.sh` or the scheduler.
8. ⛔ **Do not push, deploy, run `/fullpush`, or merge to `main`.**
9. **Check free disk space before copying** and report it. Report the total bytes copied.

## Acceptance criteria — paste real output, do not assert

- [ ] 1000 products in `moonui4_dev_be` with `company_id = 1`; the 24 demo products still present and unmodified (paste before/after counts).
- [ ] Rows created with `company_id != 1`: **0**.
- [ ] `product_images` populated; paste the distribution (how many products have 1, 2, 3+ images) and 3 sample products with their full ordered image list.
- [ ] **Live proof:** `curl -sI` the `image` URL AND every gallery URL for **3 real imported products** — all must be `200` with an `image/*` content type. This is the decisive check; the whole point of the WP is that these resolve.
- [ ] `GET /api/store/products` returns those products with working absolute URLs for main **and** gallery.
- [ ] Re-run 3× → identical counts (paste before/after).
- [ ] Zero files written outside `storage/app/public/uploads/products/`.
- [ ] `vendor/bin/pint --dirty --format agent` clean.
- [ ] `php artisan test Modules/WebStore --compact` and the Core suite — no new failures vs the baseline recorded in LEDGER.

## Commit

Conventional commit on `hazemdev4`. This is a dev fixture, so `[skip-changelog]` and **no** CHANGELOG bullet.

## Report back

Paste actual output for every check. **Flag anything in this brief that turns out to be wrong** — every work package on this project so far has found a genuine error in its own brief, including a seeder that was claimed to seed cities and seeded none. Be explicit about what you VERIFIED versus what you INFERRED. If the export's shape contradicts the assumptions above (e.g. the images are not in `attachments` at all), **STOP and report rather than improvising an import.**
