# Migration Report — PrestaShop → Laravel

**Run date:** 2026-09-09  
**Source:** `ppsanet_ps91.sql` (83 MB, prefix `tpnif_`)  
**Target:** Laravel 11 application (`laravel-proposal/app`)  
**Status:** ✅ Complete — 0 outstanding errors

---

## Summary by Entity

| Entity | Source Table(s) | Target Table | Migrated | Skipped | Errors | Notes |
|---|---|---|---|---|---|---|
| Customers | `tpnif_customer` | `users` | 6,147 | — | 0 | Includes 7 internal Cash Sale accounts |
| Products | `tpnif_product` + lang + manufacturer | `local_products` | 1,107 | — | 0 | Active products only; price ×1.15 VAT |
| Orders | `tpnif_orders` + address/carrier | `orders` | 17,409 | — | 0 | 2 fixed post-run (postcode truncation) |
| Order Items | `tpnif_order_detail` | `order_items` | 41,456 | — | 0 | Dual Turn14/local_products lookup |
| Invoices | `tpnif_order_invoice` | `order_invoices` | 17,142 | — | 0 | 12 fixed post-run (duplicate number field) |
| Order History | `tpnif_order_history` + state lang | `order_events` | 85,553 | — | 0 | |
| Order Messages | `tpnif_customer_message` + thread | `order_messages` | 9,299 | — | 0 | Order-linked threads only |
| Credit Slips | `tpnif_order_slip` | `order_credits` | 167 | — | 0 | |
| Returns | `tpnif_order_return` + detail | `order_returns` | 0 | — | 0 | PS returns table was empty |
| Newsletter | `tpnif_emailsubscription` + customers | `newsletter_subscribers` | 709 | — | 0 | Two sources merged, deduplicated by email |

---

## Data Quality Notes

### Orders — User Linkage

| Category | Count |
|---|---|
| Orders linked to a user | 16,369 |
| Orders with `id_customer = 0` (anonymous/POS) | 1,052 |
| **Total orders** | **17,421** |

The 1,052 anonymous orders have `user_id = NULL`. These were POS/counter sales entered in PS with no customer ID attached. This is expected behaviour.

### Order Items — Product Linkage

| Category | Count |
|---|---|
| Linked to Turn14 catalog product | 1,156 |
| Linked to local product | 5,446 |
| Unlinked (product not in either catalog) | 34,854 |
| **Total order items** | **41,456** |

34,854 unlinked order items represent historical orders for products no longer in the system (discontinued, deleted from PS, or not stocked in the new system). The `product_name`, `sku`, `unit_price`, and `quantity` fields are preserved from the PS snapshot so line-item data is intact even without a product FK.

### User Addresses — Legacy ID Backfill

`user_addresses.legacy_ps_id` could **not** be backfilled. The original `ppsa:import-ps-customers` command did not store `legacy_ps_id` on addresses, and the PS source data contains duplicate addresses per customer (same `address1`+`city` combination) which collided with the unique index. This does not affect any functionality — the `legacy_ps_id` on addresses was only used for idempotency on re-runs of the customer import, which has already completed.

---

## Bugs Fixed During Migration

| Issue | Root Cause | Fix |
|---|---|---|
| `chunk()` RuntimeException (MigrateProducts) | Missing `orderBy` on joined query | Added `->orderBy('p.id_product')` |
| `tpnif_carrier_lang.name` column not found | `carrier_lang` only has `delay` column, not `name` | Switched join to `tpnif_carrier as carr` |
| Duplicate invoice numbers | PS `tpnif_order_invoice.number` was reused across PS version migrations | Changed to `'PSIN' . str_pad($id_order_invoice, 6, '0', STR_PAD_LEFT)` |
| Orders 11114/11148 postcode truncation | `shipping_postal_code` is `varchar(10)` but PS stored junk (12 chars) | `substr(trim($row->postcode), 0, 10)` |
| `users.legacy_ps_id = NULL` after original customer import | `ppsa:import-ps-customers` ran before the `legacy_ps_id` migration | Backfill via email-match UPDATE (6,140 rows updated) |
| 3,412 orders with `user_id = NULL` after backfill | 7 internal Cash Sale accounts were `active=0` and skipped by original import | Imported the 7 accounts directly, re-ran orders |

---

## Prerequisite Migrations Applied

| Migration | Description |
|---|---|
| `2026_09_09_100001_add_legacy_ps_id_columns.php` | `legacy_ps_id` on 8 target tables |
| `2026_09_09_100002_add_ps_reference_to_orders.php` | `orders.ps_reference` (PS order reference string) |
| `2026_09_09_100003_create_order_returns_table.php` | `order_returns` + `order_return_items` tables |

---

## Gaps — Entities Not Migrated

These PS tables were reviewed and deliberately excluded. See `MIGRATION_GAPS.md` for full detail.

| Gap | PS Table(s) | Decision |
|---|---|---|
| G1 | `tpnif_cart` / `tpnif_cart_product` | Skip — abandoned carts have no value |
| G2 | `tpnif_product_comment` | Skip — feature TBD; table preserved |
| G3 | `tpnif_wishlist` | Skip — feature TBD |
| G4 | `tpnif_customer_thread` | Partially done — order-linked messages imported; non-order threads skipped |
| G7 | Loyalty points / credit balance | Skip — no equivalent feature in Laravel app |

---

## How to Re-Run

All commands are idempotent. To re-run a single entity:

```bash
# Preview only
ddev artisan ppsa:migrate --only=orders --dry-run

# Re-import (skip already-migrated)
ddev artisan ppsa:migrate --only=orders

# Re-import AND overwrite existing records
ddev artisan ppsa:migrate --only=orders --update-existing

# Full migration run
ddev artisan ppsa:migrate
```
