# Amazon Order Sync

Mirrors `SyncEbayOrders` — polls Amazon for confirmed orders every 5 minutes
and either writes them straight into Cydekick or pushes them into a
configured Shopify store, per channel. Before this, Amazon orders weren't
picked up anywhere at all (see the "what already exists" section below).

## Files

- `AmazonClient::getOrders()` / `getOrderItems()` — SP-API Orders v0 calls.
- `SyncAmazonOrders.php` (job) — the poller, scheduled every 5 minutes in
  `bootstrap/app.php` (`sync-amazon-orders`).
- `AmazonOrderImporter.php` — writes directly into `channel_orders_orders` /
  `channel_orders_order_lines`. Used when destination = `cydekick`.
- `AmazonToShopifyPusher.php` — pushes the order into a Shopify store via
  REST; Cydekick then picks it up through the existing Shopify order
  webhook, same as eBay's Shopify-destination path. Used when
  destination = `shopify`.
- `Channel::amazonOrderDestination()` / `amazonOrderShopifyChannelId()` —
  read from `channels_channels.settings` (`amazon_order_destination`,
  `amazon_order_shopify_channel_id`), same as the eBay equivalents. No
  migration needed for these two — `settings` is already a JSON column.
- Migration `2026_09_18_000001_add_amazon_order_sync_support.php` — adds
  `channels_channels.amazon_orders_synced_to` (the cursor) and creates
  `channels_amazon_order_syncs` (the audit table), both structurally
  identical to eBay's equivalents.

## Structural differences from eBay, forced by the Orders API v0 itself

1. **Envelope**: Orders API v0 wraps everything in a top-level `"payload"`
   key — unlike every other Amazon endpoint used elsewhere in this codebase
   (Listings Items 2021-08-01, Catalog 2022-04-01), which return data
   directly. `getOrders()`/`getOrderItems()` unwrap this internally so
   callers never see it.
2. **Pagination**: `getOrders()` returns at most ~100 orders per call. The
   job follows `NextToken` in a loop until exhausted before processing
   anything.
3. **Line items are a separate call.** `GetOrders` never includes them —
   `getOrderItems($amazonOrderId)` is called once per order, and the result
   is injected as `$order['OrderItems']` before handing the order to either
   the importer or the pusher, both of which expect that shape already
   present (mirrors how eBay's single API response already contains
   `lineItems` inline — Amazon just doesn't work that way).
4. **Status vocabulary is different.** eBay uses payment status
   (`PAID`/`FULLY_PAID`/etc); Amazon orders are always pre-paid by the time
   they're visible via the API, so the equivalent filter is `OrderStatus`:
   only `Unshipped`, `PartiallyShipped`, `Shipped` are imported (skips
   `Pending` — payment still authorizing — and `Canceled`/`Unfulfillable`).
5. **FBA orders are deliberately excluded.** `FulfillmentChannel = 'AFN'`
   (Fulfilled by Amazon) orders are skipped entirely — Amazon's own
   warehouses pick, pack, and ship those independently, so Cydekick has no
   fulfillment role for them. Importing them would create phantom "needs
   dispatch" records and risk deducting stock Cydekick never actually held.
   Only `'MFN'` (merchant-fulfilled) orders are imported. If FBA order
   *reporting* (not fulfillment) is ever wanted, this filter is the one
   line to relax — but that's a deliberately separate decision, not done
   here.

## What already existed before this (and still does, unchanged)

`AmazonOrdersController` (`POST /webhooks/amazon/orders`) — a pre-existing
webhook endpoint that writes directly into Cydekick's own order table, no
Shopify path at all. Left untouched since it doesn't conflict with the new
polling job (different mechanism, and idempotency on `external_id` means a
duplicate delivery from both paths would just get skipped, not double
counted). Worth knowing it's very likely dead code in practice — Amazon's
SP-API doesn't push webhooks in this simple REST-POST shape; real Amazon
order notifications go through AWS SQS/EventBridge subscriptions, a
fundamentally different mechanism nothing here sets up. The new
`SyncAmazonOrders` polling job is the one that actually works, confirmed
against the live API.

## Configuring a channel's destination

Same settings-JSON pattern as eBay — no UI built for this yet, set directly:

```php
DB::table('channels_channels')->where('id', $channelId)->update([
    'settings' => json_encode(array_merge(
        json_decode(DB::table('channels_channels')->where('id', $channelId)->value('settings') ?? '{}', true),
        [
            'amazon_order_destination' => 'shopify', // or 'cydekick' (default)
            'amazon_order_shopify_channel_id' => 15, // required only if destination = shopify
        ]
    )),
]);
```

## Related, added later (2026-09-28)

- **SKU linking fallback** and **pushing tracking back to Amazon** (including for orders pushed to Shopify via
  `amazonOrderDestination = 'shopify'`) are covered in `order-dispatch-fulfillment.md`, not here.
- **Per-run message column**: `channels_amazon_order_syncs` gained a `message` text column. Each run now writes
  one line per order (`Imported …`, `Skipped … — already imported`, `Skipped … — status … (not yet confirmed)`,
  `Skipped … — Fulfilled by Amazon`, `FAILED …: <reason>`) instead of only a top-level `error_message`, and the
  Order Sync page shows it as a Message column in Recent syncs (red text on a failed/partial run, grey otherwise).
  Mirrored on `channels_ebay_order_syncs` / eBay's Order Sync tab the same way.
- **Order Sync tab now shared with eBay**: `ViewOrderSync` (channels resource page) used to be eBay-only; it's
  now driven by `ChannelPlatform::supportedTabs()` and shows Amazon's own cursor/destination/history using the
  same page (wording, cursor column and dispatched job switch on `$this->slug()` = `'amazon'`/`'ebay'`).

## Testing

Dispatch manually rather than waiting for the 5-minute schedule:

```php
\Webkul\Channel\Jobs\SyncAmazonOrders::dispatch($channelId);
```

Check `channels_amazon_order_syncs` for the outcome of each run, same as
`channels_ebay_order_syncs`. Verified against the real Somerset4x4 Amazon
channel (id 14) on 2026-09-18: ran cleanly, `0 imported` (no real orders in
the test account in the last 24h), zero errors — confirms the API calls,
pagination, and cursor/audit mechanics all work, though the actual
import/push code paths (`AmazonOrderImporter`/`AmazonToShopifyPusher`)
haven't been exercised against a real order yet since none existed to test
with.
