# Shopify Order Sync

## Overview

The Order Sync feature imports Shopify orders into the Channel Orders plugin. It runs as a background job and can be triggered manually from the Channels admin panel. Orders are de-duplicated automatically — re-running the sync will never create duplicate orders.

---

## Where to find it

1. Go to **Channels** in the left-hand navigation
2. Open a Channel record (a Shopify channel)
3. Click the **Order Sync** tab in the sub-navigation (next to SKU Sync)

---

## How to use it

### Import Orders

Click **Import Orders**. This dispatches the background job immediately. The page auto-refreshes every 5 seconds while the job is running. When it finishes, a summary is shown:

```
Last synced 2 minutes ago · ✓ 42 imported  18 skipped  0 errors
```

- **Imported** — new Shopify orders that were saved to Channel Orders
- **Skipped** — orders already in the system (matched by Shopify order ID)
- **Errors** — orders that failed to import (details logged to the Laravel log)

### Reset Cursor

The sync is **incremental** — after the first run, only orders newer than the last imported Shopify order ID are fetched. Click **Reset Cursor** to clear this marker, so the next sync re-checks all Shopify orders from the beginning. Already-imported orders are always skipped by `external_id` check, so there is no risk of duplicates.

---

## What gets imported

### Order record

| Field | Source |
|---|---|
| `name` | Auto-generated (CO/1001, CO/1002, …) |
| `external_id` | Shopify order ID (used for de-duplication) |
| `external_channel_reference` | Shopify order name (e.g. #1001) |
| `status` | Always `open` on import |
| `payment_status` | `paid` if Shopify `financial_status = paid`, otherwise `unpaid` |
| `subtotal` | Shopify `subtotal_price` |
| `tax_amount` | Shopify `total_tax` |
| `delivery_cost` | Shopify `total_shipping_price_set.shop_money.amount` |
| `total_amount` | Shopify `total_price` |
| `currency_code` | Shopify `currency` |
| `shipping_address` | Shopify shipping address stored as JSON |
| `created_at` | Backdated to the Shopify order creation date |

### Order lines

Each Shopify `line_item` becomes one order line.

| Field | Source |
|---|---|
| `sku` | Shopify variant SKU |
| `name` | Shopify `title` + `variant_title` |
| `quantity` | Shopify `quantity` |
| `unit_price` | Shopify `price` |
| `subtotal` | `quantity × unit_price` |
| `tax_rate` | First tax line rate × 100 (%) |
| `tax_amount` | Sum of all `tax_lines[].price` on the line |
| `line_total` | `subtotal + tax_amount` |
| `product_id` | Resolved via SKU mapping (see below) |

### Customer contact

The job tries to find an existing contact by **email address**. If none is found, a new `Partner` record is created (Individual account type) with:

- Name (first + last from Shopify customer)
- Email, Phone
- Billing address (from Shopify `billing_address`) stored as a child address record with `sub_type = invoice`
- Delivery address (from Shopify `shipping_address`) stored as a child address record with `sub_type = delivery`, only if it differs from billing

Contacts are viewable and editable in the **Contacts** section of the admin.

---

## Linked vs Unlinked lines

A line is **linked** if its Shopify SKU is found in the **SKU Mappings** for this channel (Channels → SKU Mappings tab).

| State | `product_id` | `name` prefix | Description |
|---|---|---|---|
| Linked | Set to Cydekick product ID | None | Fully connected to inventory |
| Unlinked | `null` | `[UNLINKED]` | Imported but not tied to inventory |

**Unlinked lines are never discarded.** The order is still imported in full — the line just has no `product_id`. No inventory product is created automatically.

To resolve unlinked lines:

1. Go to the **SKU Mappings** tab for the channel
2. Add a mapping for the missing Shopify SKU to a Cydekick product
3. Run **Reset Cursor** then **Import Orders** again

> Note: re-importing will skip the order (already exists by `external_id`). Only new orders will pick up the new mapping. To re-link existing lines, those would need to be updated manually in Channel Orders.

---

## Background job details

**Job class:** `Webkul\Channel\Jobs\SyncShopifyOrders`

**Queue:** default Laravel queue (run with `php artisan queue:work`)

**Timeout:** 600 seconds (10 minutes)

**Retries:** 2

**Cache keys used:**

| Key | Purpose |
|---|---|
| `shopify_order_sync_running_{channelId}` | Set to `true` while job is running, cleared on finish |
| `shopify_order_sync_since_id_{channelId}` | Highest Shopify order ID seen — used for incremental fetch |
| `shopify_order_sync_last_at_{channelId}` | ISO timestamp of last completed sync |
| `shopify_order_sync_last_result_{channelId}` | Array: `imported`, `skipped`, `errors`, `total`, `ran_at` |

---

## Files

| File | Purpose |
|---|---|
| `plugins/webkul/channels/src/Services/ShopifyClient.php` | `getOrders()` method — calls Shopify REST API |
| `plugins/webkul/channels/src/Jobs/SyncShopifyOrders.php` | Background job — imports orders, creates contacts/lines |
| `plugins/webkul/channels/src/Filament/Resources/ChannelResource/Pages/ViewOrderSync.php` | Filament page — UI, actions, computed data |
| `plugins/webkul/channels/resources/views/filament/pages/order-sync.blade.php` | Blade view — order table, stats, controls |
| `plugins/webkul/channels/src/Filament/Resources/ChannelResource.php` | Registered `order-sync` route and sub-nav tab |

---

## Requirements

- The channel must have valid Shopify API credentials (`shop_domain`, `access_token`)
- The queue worker must be running: `php artisan queue:work` (see `running_workers.rmd`)
- SKU Mappings should be configured before syncing for best results (unlinked lines still import)
