# eBay Connector — Planning Context for Claude

Use this document as the full context prompt when asking Claude to produce a detailed
implementation plan for the eBay connector. Read the whole document before asking any
questions; it contains all the decisions already made and the reasoning behind them.

---

## 1. What the channels plugin currently does (Shopify)

The `channels` plugin (`plugins/webkul/channels/`) is the multi-channel sales connector
for Cydekick. Its job is to keep product data, prices, stock levels, and orders in sync
between the ERP and one or more external marketplaces.

### Core data model

| Table | Purpose |
|---|---|
| `channels_channels` | One row per external channel. Stores credentials (encrypted), platform enum, settings JSON, connection status |
| `channels_sku_mappings` | Links a Cydekick product (`product_id`) to an external variant ID (`shopify_variant_id`), inventory item ID, and product ID. One row per product per channel |
| `channel_listings` | Generic listing status tracking — `status`, `last_synced_at`, `last_error`, `update_reason`, `external_listing_id` |
| `listing_configurators` | Channel-level templates: title template, pricing rule (markup %), default vendor/type/tags |
| `channels_product_prices` | Per-product, per-channel price override (compare-at price too) |
| `channels_sku_sync_status` | Per-SKU sync health: last push time, Shopify inventory item ID, last known quantity |
| `channels_shopify_variants` | Caches Shopify variant data locally |
| `channels_shopify_locations` | Caches Shopify fulfilment location IDs |
| `channels_location_mappings` | Maps a Cydekick warehouse to a Shopify location |

### Key abstractions

**`ChannelPlatform` enum** (`src/Enums/ChannelPlatform.php`)
Lists supported platforms: `shopify`, `manual`, `default`. Each case has a
`getDriverClass()` method that returns the PHP class to instantiate as the driver.
To add eBay: add `case Ebay = 'ebay'` and point `getDriverClass()` at the new client.

**`ChannelDriverInterface`** (`src/Contracts/ChannelDriverInterface.php`)
The contract every driver must satisfy. Current methods are Shopify-centric (inventory
levels, variant price updates, etc.). For eBay a new interface or extended interface
will be needed — see Section 3.

**`ShopifyClient`** (`src/Services/ShopifyClient.php`)
Implements `ChannelDriverInterface`. Handles REST and GraphQL calls, OAuth token
exchange, rate-limit retries, webhook HMAC verification. eBay gets its own `EbayClient`.

**`Channel` model** (`src/Models/Channel.php`)
Platform-agnostic. Stores `api_key`, `api_secret` (encrypted), `access_token`
(encrypted), `settings` (JSON bag for anything platform-specific). Resolves the
correct driver via `$channel->driver()`.

### UI — ManageListingsV2

`ManageListingsV2` (`src/Filament/Pages/ManageListingsV2.php`) is a Livewire 3 page
(not a Filament Resource) with:
- URL-based channel selection (`?channel=<id>`)
- Tabs: Listed / Needs Update / Not Synced / Not Linked / Errors
- Alpine.js floating bulk-action bar (sticky, fixed-position)
- Bulk actions: Push to channel, Create in channel, Unlink, Mark needs update, Cancel sync
- Lazy-loaded product table (Livewire `$page`, `$perPage`, `$search`)

The same page serves all channels — it reads `$channelId` from the URL and queries
`channel_listings` filtered by `channel_id`. The eBay connector will share this page.

### Job architecture (Shopify)

| Job | What it does |
|---|---|
| `ShopifyBatchSyncJob` | Bulk-updates up to 250 products via Shopify's Bulk Operations GraphQL API. Phases: DB preload → JSONL build → bulk op → image REST → barcode/weight → publish → inventory → fitment metafields → DB writes |
| `ShopifyBatchCreateJob` | Creates new Shopify listings for products that have never been listed |
| `SyncShopifyOrders` | Polls Shopify order API, creates `channel_orders` records |
| `SyncProductPriceToChannel` | Single-product price push (triggered by price change observer) |
| `SyncSkuInventoryStatus` | Polls Shopify for current inventory, compares to ERP stock |

All jobs use `JobLogger` (`plugins/webkul/console/`) for real-time feedback in the
Cydekick console panel.

### Observer hooks

- `ProductObserver` — fires when a product is saved; dispatches `SyncProductToChannels`
  if the product has active listings and the channel is active
- `ProductQuantityObserver` — fires on stock change; pushes updated inventory
- `ChannelProductPriceObserver` — fires when a price row is updated; pushes new price

---

## 2. Why eBay is architecturally different from Shopify

Understanding the differences is essential before designing the eBay connector.

| Concern | Shopify | eBay |
|---|---|---|
| **Auth** | Custom OAuth app (permanent access token after one exchange) | OAuth 2.0 with refresh tokens; access tokens expire in 2 hours; must store and refresh |
| **API style** | REST + GraphQL hybrid; bulk operations via JSONL upload | REST only (Sell Inventory API v1, Fulfillment API, Account API) |
| **Product model** | Product → Variant → InventoryItem. One product, many variants | Inventory Item → Offer. Each SKU is a separate Offer attached to a Listing |
| **Bulk operations** | Shopify-managed batch via JSONL; poll for completion | No equivalent; must loop per-offer (rate-limited ~5000 calls/day at Basic level) |
| **Categories** | Product type string | Mandatory eBay category ID (Browse/Category Tree API to look up) |
| **Item specifics** | Metafields (free-form) | Structured key-value pairs per category (Taxonomy API to validate) |
| **Condition** | Not enforced | Mandatory condition ID (1000 = New, 3000 = Used, etc.) |
| **Stock** | Inventory level at location | Quantity on the Offer |
| **Pricing** | Variant price; compare-at for sale | BuyItNow price on the Offer; no compare-at natively (UK eBay allows price reduction with strikethrough via `minimumAdvertisedPrice`) |
| **Images** | Shopify-hosted via REST, then synced | eBay-hosted — must upload image URLs; eBay fetches and re-hosts them |
| **Listings** | Products are published/unpublished | Offers go `UNPUBLISHED` → `PUBLISHED` (active listing) → `ENDED` |
| **Fees** | Shopify monthly fee only | Per-listing insertion fees + final value fees; connector doesn't manage fees but pricing rules should account for them |
| **Orders** | Full order sync including fulfilment | Fulfilment API gives orders; must submit tracking back to eBay or penalty applies |
| **Sandbox** | Shopify dev store | eBay has a full sandbox environment with separate credentials |

---

## 3. Proposed architecture for the eBay connector

### 3.1 What stays the same (reuse as-is)

- `Channel` model — add no new columns; all eBay config goes in the `settings` JSON
- `channel_listings` — generic enough; `external_listing_id` stores eBay offer ID
- `listing_configurators` — reuse for eBay-specific defaults (condition, category, policy IDs)
- `ManageListingsV2` page — same UI, same tabs, same bulk bar; only new Livewire actions needed
- `JobLogger` — all eBay jobs use this for console feedback
- `ChannelPlatform` enum — add `case Ebay = 'ebay'`

### 3.2 New database tables

| Table | Purpose |
|---|---|
| `channels_ebay_sku_mappings` | eBay-specific variant: links `product_id` → `ebay_offer_id`, `ebay_listing_id`, `ebay_inventory_item_id` (=SKU on eBay), `condition_id`, `category_id` |
| `channels_ebay_tokens` | OAuth token store: `channel_id`, `access_token` (encrypted), `refresh_token` (encrypted), `expires_at`. Separate from `channels_channels.access_token` because eBay tokens expire and rotate |
| `channels_ebay_policies` | Caches eBay fulfilment/return/payment policy IDs per channel (fetched from Account API on connect) |

> **Decision**: Keep `channels_sku_mappings` Shopify-only; create a parallel
> `channels_ebay_sku_mappings` table. The generic `channel_listings` bridges both via
> `external_listing_id`. This avoids polluting the Shopify mapping table with nullable
> eBay columns.

### 3.3 `EbayClient` service

`plugins/webkul/channels/src/Services/EbayClient.php`

Implements a new `EbayDriverInterface` (extends `ChannelDriverInterface` or replaces it
for eBay). Key methods:

```php
// Auth
public function getAuthorizationUrl(string $redirectUri, string $state): string;
public function exchangeCodeForTokens(string $code): array; // returns access+refresh+expires
public function refreshAccessToken(): string;               // rotates token in DB

// Inventory API (Sell)
public function createOrReplaceInventoryItem(string $sku, array $data): bool;
public function getInventoryItem(string $sku): ?array;
public function createOffer(array $offerData): string;       // returns offerId
public function updateOffer(string $offerId, array $data): bool;
public function publishOffer(string $offerId): string;       // returns listingId
public function withdrawOffer(string $offerId): bool;
public function bulkCreateOffer(array $offers): array;       // max 25 per call
public function bulkMigrateListings(array $listingIds): array;

// Inventory
public function updateQuantity(string $sku, int $quantity, string $merchantLocationKey): bool;

// Orders (Fulfillment API)
public function getOrders(array $params = []): array;
public function submitTracking(string $orderId, array $trackingData): bool;

// Account API (policies)
public function getFulfillmentPolicies(): array;
public function getPaymentPolicies(): array;
public function getReturnPolicies(): array;

// Category / Taxonomy
public function getCategoryTree(string $categoryTreeId = '3'): array; // 3 = UK
public function getCategoryAspects(string $categoryId): array;        // item specifics schema
```

### 3.4 Jobs

| Job | Equivalent Shopify job | Notes |
|---|---|---|
| `EbayBatchSyncJob` | `ShopifyBatchSyncJob` | Loops products in batches of 25 (eBay's `bulkCreateOffer` limit). Phases: DB preload → createOrReplaceInventoryItem per SKU → updateOffer (or createOffer if new) → publishOffer if unpublished → update quantity → DB writes |
| `EbayBatchCreateJob` | `ShopifyBatchCreateJob` | Creates inventory items + offers + publishes for new listings |
| `SyncEbayOrders` | `SyncShopifyOrders` | Polls Fulfillment API, creates channel-orders records, submits tracking when fulfilment status changes |
| `RefreshEbayTokenJob` | *(no Shopify equivalent)* | Scheduled every 90 minutes via `$schedule->job()` in the ServiceProvider. Refreshes expiring tokens before each sync job runs |

### 3.5 OAuth flow (eBay differs from Shopify)

Shopify OAuth is triggered from the Channel edit page (a button calls `ShopifyAuthController`).

eBay uses the same pattern but with a different controller:

1. Admin clicks "Connect to eBay" on Channel edit page
2. `EbayAuthController::redirect()` builds the eBay consent URL (scope: `sell.inventory`, `sell.fulfillment`, `sell.account`, `commerce.catalog`) and redirects
3. eBay redirects back to `/channels/ebay/callback?code=...&state=...`
4. `EbayAuthController::callback()` calls `EbayClient::exchangeCodeForTokens()`, stores in `channels_ebay_tokens`, marks channel `status = connected`
5. `RefreshEbayTokenJob` handles subsequent renewal automatically

### 3.6 Filament UI changes

**ChannelResource form** — when `platform = ebay` is selected, show eBay-specific fields:
- `settings.ebay_site_id` — UK = 3, default 3
- `settings.merchant_location_key` — eBay inventory location key
- `settings.fulfillment_policy_id`, `settings.payment_policy_id`, `settings.return_policy_id`
- `settings.default_condition_id` — e.g. 1000 (New)
- `settings.default_category_id` — default eBay category for uncategorised products

Use Filament's `visible(fn ($get) => $get('platform') === 'ebay')` pattern — already used
in ChannelResource for Shopify-specific fields.

**ManageListingsV2** — new bulk actions appear when the selected channel is eBay:
- "Push to eBay" (updates offers) — maps to `EbayBatchSyncJob`
- "Create on eBay" (creates + publishes) — maps to `EbayBatchCreateJob`
- "End Listing" (withdrawOffer) — no Shopify equivalent; ends the active listing

The bulk bar already conditionally renders buttons based on `$this->statusFilter`; extend
the same `@if`/`@elseif` chain to handle `platform === 'ebay'` or add a new condition.

**ListingConfigurator** — add eBay-specific fields:
- `condition_id` (select — eBay condition IDs)
- `category_id` (text — eBay leaf category ID)
- `item_specifics` (key-value JSON — required specifics for the category)

---

## 4. What to reuse exactly vs. what to write fresh

### Reuse exactly (no changes)
- `Channel` model
- `ChannelListing` model
- `ListingConfigurator` model (add new columns, don't replace)
- `ManageListingsV2` Livewire page class (add eBay actions, don't fork)
- `manage-listings-v2.blade.php` view (extend `@elseif` chain in bulk bar)
- `JobLogger` service
- `ChannelServiceProvider` (register new migrations + jobs)
- `ChannelPlugin` (register new pages if any)

### Add new cases/values
- `ChannelPlatform` enum — add `Ebay`
- `ChannelResource` form — add eBay conditional fields
- `ChannelDriverInterface` — review; eBay may need additional methods that aren't
  relevant to Shopify (e.g. `getCategoryAspects`, token refresh)

### Write fresh (new files)
- `EbayClient.php` — API client / driver
- `EbayAuthController.php` — OAuth redirect + callback
- `EbayBatchSyncJob.php` — bulk update job
- `EbayBatchCreateJob.php` — bulk create + publish job
- `SyncEbayOrders.php` — order polling job
- `RefreshEbayTokenJob.php` — token refresh scheduled job
- Migrations: `channels_ebay_sku_mappings`, `channels_ebay_tokens`, `channels_ebay_policies`
- Views: any eBay-specific Filament sub-pages (policy picker, category search)

---

## 5. eBay API credentials / environment

- Production: `developer.ebay.com` → Application Keys
- Sandbox: separate set of credentials; sandbox API base URL is `api.sandbox.ebay.com`
- `settings.ebay_environment` = `'production'` | `'sandbox'` — determines base URL in `EbayClient`
- Required scopes: `https://api.ebay.com/oauth/api_scope/sell.inventory`, `sell.fulfillment`, `sell.account`, `commerce.catalog.readonly`

Store in `channels_channels`:
- `api_key` → eBay App ID (Client ID)
- `api_secret` → eBay Cert ID (Client Secret) — already encrypted
- `access_token` — short-lived OAuth token (2h) — encrypted
- `channels_ebay_tokens.refresh_token` — long-lived (18 months) — encrypted separately

---

## 6. Pricing considerations specific to eBay

The `channels_product_prices` table already stores per-product, per-channel prices.
For eBay, the price pushed via `updateOffer` is the `pricingSummary.price`. Use the
existing `ChannelProductPrice` record as the source.

eBay UK allows a "Was / Now" crossed-out price but only under strict conditions (the
item must have been listed at the higher price for 28 days). Do not implement compare-at
automatically — add a `settings.enable_strike_through_price` flag, default false.

---

## 7. Stock / inventory location

eBay Inventory API requires a Merchant Location Key (a string like `"warehouse-1"`).
The location is created via `POST /sell/inventory/v1/location/{merchantLocationKey}`.
Store the key in `channels_channels.settings.merchant_location_key`.

One location per channel is sufficient for most cases (Somerset4x4 ships from one
warehouse). The `channels_location_mappings` table maps Cydekick warehouse → external
location; add an `ebay_merchant_location_key` column or store in the settings JSON.

---

## 8. Order sync for eBay

eBay orders arrive via the Fulfillment API (`GET /sell/fulfillment/v1/order`). Key
differences from Shopify:

- eBay orders already include buyer details, line items with SKUs, and payment status
- Must acknowledge the order within 5 days (mark as `FULFILLED`) or eBay may take action
- Must submit tracking via `POST /sell/fulfillment/v1/order/{orderId}/shippingFulfillment`
- Channel-orders plugin already handles Shopify orders — for eBay, add an `EbayOrderImporter`
  (mirror of `ShopifyOrderImporter`) that maps eBay payload fields to the same `Order` model

---

## 9. Files in the codebase to read before planning in detail

If asking Claude to plan or implement any specific section, always read these first:

| File | Why |
|---|---|
| `plugins/webkul/channels/src/Contracts/ChannelDriverInterface.php` | Interface all drivers must implement |
| `plugins/webkul/channels/src/Services/ShopifyClient.php` | Complete driver implementation to mirror |
| `plugins/webkul/channels/src/Enums/ChannelPlatform.php` | Where to add `Ebay` case |
| `plugins/webkul/channels/src/Models/Channel.php` | Channel model + driver resolver |
| `plugins/webkul/channels/src/Models/SkuMapping.php` | Shopify mapping model to parallel |
| `plugins/webkul/channels/src/Models/ChannelListing.php` | Generic listing model (shared) |
| `plugins/webkul/channels/src/Jobs/ShopifyBatchSyncJob.php` | Sync job to parallel |
| `plugins/webkul/channels/src/Jobs/ShopifyBatchCreateJob.php` | Create job to parallel |
| `plugins/webkul/channels/src/Filament/Pages/ManageListingsV2.php` | Livewire page to extend |
| `plugins/webkul/channels/resources/views/filament/pages/manage-listings-v2.blade.php` | View to extend |
| `plugins/webkul/channels/src/Filament/Resources/ChannelResource.php` | Form to extend with eBay fields |
| `plugins/webkul/channels/src/Http/Controllers/ShopifyAuthController.php` | OAuth flow to mirror |
| `plugins/webkul/channels/routes/web.php` | Where to add eBay OAuth routes |
| `plugins/webkul/channels/src/ChannelServiceProvider.php` | Where to register migrations + jobs |
| `plugins/webkul/channel-orders/src/Services/ShopifyOrderImporter.php` | Order importer to mirror |
| `plugins/webkul/console/src/Services/JobLogger.php` | Console logging API |

---

## 10. Suggested prompt to generate the full plan

Paste the following (with this document attached or as prior context):

> "Using the ebay-connector-plan.md document as context, generate a detailed
> step-by-step implementation plan for the eBay connector. The plan should cover:
> (1) all new migrations with schema, (2) EbayClient methods with signature and
> return types, (3) OAuth flow controller, (4) EbayBatchSyncJob phases in the same
> structure as ShopifyBatchSyncJob, (5) EbayBatchCreateJob phases, (6) RefreshEbayTokenJob,
> (7) SyncEbayOrders job, (8) UI changes to ChannelResource form and ManageListingsV2
> blade, (9) ServiceProvider registration changes.
> For each section, identify which existing file to read/copy from and what specifically
> changes. Do not implement — produce the plan only."
