# Reimporting a Single Shopify Order

`php artisan channels:reimport-shopify-order {order}` — deletes one already-imported order
locally and re-fetches it fresh from Shopify, so it picks up an importer/field change without a
full re-sync or hand-editing the DB via tinker. Built 2026-09-30 alongside the Shopify discount
import work below, after doing this by hand via tinker twice in the same session.

## Files

- `ReimportShopifyOrderCommand.php` (console command, `plugins/webkul/channels`) — the whole thing.
- `ShopifyClient::getOrder(string $shopifyOrderId): ?array` — single-order fetch, added for this;
  previously only `getOrders()` (bulk, paginated) existed. Shares one field whitelist,
  `ShopifyClient::ORDER_FIELDS`, with `getOrders()` so a single-order re-fetch can never drift out
  of sync with what the bulk sync pulls (see the whitelist gotcha below — this is exactly the bug
  class that motivated sharing the constant).
- `ShopifyOrderImporter::createFromPayload()` — same create path used by the bulk sync job and the
  order webhook; the reimport command is a third caller of the same method, nothing bespoke.

## Usage

```
php artisan channels:reimport-shopify-order C1040
php artisan channels:reimport-shopify-order "#1440"     # Shopify's own order ref also works
php artisan channels:reimport-shopify-order C1040 --yes  # skip the confirmation prompt
```

Finds the order by Cydekick name (`C1040`) or Shopify ref (`#1440`/`1440`), prints a one-line
summary, asks for confirmation, deletes the order + its lines, re-fetches from Shopify, reimports,
then prints the new subtotal/discount/tax/delivery/total/status for a quick sanity check.

## Why delete + recreate, not update-in-place

`ShopifyOrderImporter` only ever has `createFromPayload()` — there is no update path anywhere in
the codebase (webhook and sync job are both create-only too; see `channel-listings.md` /
`amazon-order-sync.md` for the same pattern on other plugins). Reimporting therefore has to delete
the old row and create a new one through the real import path, rather than patching fields
in-place — this is deliberate: it's the only way to prove the *actual* import code produces the
right result, not just that a manually-patched field looks right.

**Consequence worth knowing**: the order gets a new internal `channel_orders_orders.id` (it keeps
the same `name`, e.g. `C1040`, since `order_number` is `max(withTrashed) + 1` and the old row is
force-deleted, not soft-deleted, so it no longer counts toward that max). Nothing in this codebase
currently holds a hard FK to `Order.id` from outside `channel-orders` itself (checked — only
`ViewOrderSync.php`, `SyncAmazonOrders`/`SyncEbayOrders`, and `PoCreate.php` reference the model,
none by a stored foreign id), so this is low-risk today, but it's the reason the command warns
about it in its own output.

## The safety guard (`--force`)

Refuses to touch an order whose `status` is already `processed` or `cancelled`, unless `--force`
is passed. Reason: `ShopifyOrderImporter::createFromPayload()` fires `OrderObserver` stock
deduction + channel-sync dispatch for any order it creates that's already fulfilled
(`$isFulfilled` branch, mirrors the webhook/sync path exactly). Deleting a *processed* order and
reimporting it re-creates it as "new" from that observer's point of view, so the deduction fires
again — silently taking stock off a second time for something already shipped. An `open`/
unfulfilled order (the normal case for "I just fixed the importer, let me re-pull this one order
to check") never went through that branch in the first place, so it's safe by default.

## Background: the Shopify discount bugs this command was built to re-verify

Three real, separate bugs found and fixed the same session, all in the Shopify order import path —
worth knowing if touching this area again:

1. **`ShopifyClient::getOrders()`'s `fields=` whitelist didn't include `total_discounts`,
   `discount_codes`, or `discount_applications`.** No amount of correct importer logic can recover
   a field Shopify was never asked to return in the first place — this was the actual root cause,
   not the importer. Caught by testing against a live dev-store order and seeing the discount
   simply absent. **The lesson**: when a Shopify field isn't showing up despite payload parsing
   looking right, check this whitelist before anything else.
2. **A manually-applied discount (Shopify's own "Custom Discount", added in the order editor, no
   checkout code) doesn't appear in `discount_codes`** — that array is checkout-codes-only.
   `discount_applications` covers all discount sources uniformly (`type: manual|discount_code|
   script|automatic`), with `code` only present for the `discount_code` type and `title` (often
   literally "Custom Discount") for the rest. The importer reads `discount_applications` first,
   falling back to `discount_codes` only if that key is ever absent from a payload variant.
3. **Subtotal was stored net of discount, double-counting the discount on display.** Shopify's own
   `subtotal_price` field is defined as *post*-discount (before shipping/tax) — but Cydekick's
   order lines store each item's full undiscounted price, so their sum (and
   `Order::recalculateTotals()`, which derives subtotal by summing lines) is the *gross*
   pre-discount total. Storing `subtotal_price` as-is meant the UI showed "Subtotal: £X, Discount:
   -£Y, Total: £X" — the £Y never actually left the subtotal shown. Fixed by adding
   `discount_amount` back onto `subtotal_price` at import time so `subtotal − discount = total`
   actually reconciles, both at import and every time `recalculateTotals()` re-derives it from
   line edits afterward.

`discount_code` / `discount_amount` are plain columns on `channel_orders_orders` (migration
`2026_09_30_000001_add_discount_to_channel_orders_orders.php`), shown on the Channel Orders list,
the order detail totals block, and the channel's own "View Order Sync" preview table.

### Gotcha: this migration didn't actually run on first deploy

Hit the "migration file exists but `migrate --force` says Nothing to migrate" issue on this exact
migration when first deployed — full explanation, cause, and fix now live in their own note since
it's a recurring, codebase-wide issue, not specific to this feature: see
`plugin-migrations-not-registered.md`.

**Only new/reimported orders get this** — nothing retroactively backfills discount data on orders
already imported before this fix landed; use this command one order at a time for any that matter,
there's no bulk backfill command for it (not built — ask if a batch one is actually needed).
