# eBay Connector — Developer Notes


# App  -https://developer.ebay.com/

james.banwell@somerset4x4.co.uk -(#Asshole73299549) - Live - Farm remove
email@jamesbanwell.co.uk - (#Asshole73269549) - dev local  - Farm remove

# local use a npx tunnel to get the https.

Ebay dev keys are set with this callback url https://great-grapes-begin/channels/ebay/callback

#so start a tunnel using 

npx localtunnel --port 80 --subdomain great-grapes-begin


## for a complete sandbox ebay account to test 
## Sandbox Credentials

| | |
|---|---|
| **Sandbox test user** | `TESTUSER_jamesbanwell` |
| **Sandbox test password** | `#Asshole69` |
| **Developer account** | `jamesbanwell` at developer.ebay.com |
| **Sandbox App ID** | `jamesban-PWLRPart-SBX-855b67044-660d3427` |
| **Sandbox Cert ID** | `SBX-55b670444d03-ca68-46c6-8fd3-5a70` |
| **Sandbox RuName** | `james_banwell-jamesban-PWLRPa-mkdwyxovs` |
| **Production App ID** | `jamesban-PWLRPart-PRD-f55b67044-c0ed5850` |
| **Production Cert ID** | `PRD-55b67044fead-bc69-4065-953e-ba5c` |

Sandbox test users managed at: developer.ebay.com/my/test_users

---

## Cydekick Channel Setup (Sandbox)

| Field | Value |
|---|---|
| Channel name | Sandbox |
| Platform | eBay |
| App ID | Sandbox App ID above |
| Cert ID | Sandbox Cert ID above |
| RuName | Sandbox RuName above |
| Marketplace | EBAY_GB |
| Environment | Sandbox (testing) |

Callback URL registered in eBay developer portal:
`https://cydekick.co.uk/channels/ebay/callback`

---

## eBay Developer Portal Gotchas

### RuName (not a URL)
eBay OAuth uses a **RuName** (Redirect URL Name) as the `redirect_uri` parameter — NOT the actual callback URL. The RuName is a string like `james_banwell-jamesban-PWLRPa-mkdwyxovs` that eBay internally maps to the registered callback URL.

Found at: developer.ebay.com → your app → User Tokens → eBay Redirect URL name column.

### OAuth vs Auth'n'Auth
When registering a redirect URL, the radio button must be set to **OAuth** (not Auth'n'Auth). Auth'n'Auth is eBay's legacy login system.

### HTTPS required
eBay requires HTTPS for the auth accepted URL. `http://localhost` is rejected. For local testing, use the production server URL and test from there — sandbox credentials work against the production callback URL.

### Sandbox/Production RuName mismatch
Sandbox keysets generate Sandbox RuNames (contains `SBX` or `PWLRPa`). Production keysets generate Production RuNames. They cannot be mixed. Separate User Token entries are needed for each environment.

### `commerce.taxonomy.readonly` scope
This scope does NOT appear in the app's granted scopes list — eBay requires explicit approval for it. Taxonomy API public endpoints work with the basic `https://api.ebay.com/oauth/api_scope` scope instead. The connector does not request `commerce.taxonomy.readonly`.

### Business Policies mandatory
eBay's Sell Inventory API requires Business Policies to be set up before any listing can be published. Payment, Fulfillment, and Return policy IDs are required fields on `createOffer`. These are synced automatically on first OAuth connect and stored in `channels_ebay_policies`.

---

## Architecture

### Database tables
| Table | Purpose |
|---|---|
| `channels_ebay_tokens` | Access + refresh tokens (encrypted), expiry timestamps |
| `channels_ebay_policies` | Business policies cached from Account API (fulfillment/payment/return) |
| `channels_ebay_configurators` | Named listing templates (Linnworks-equivalent) |
| `channels_ebay_sku_mappings` | Per-product eBay SKU, offer ID, listing ID |
| `channels_channels` | Added `ebay_marketplace_id`, `ebay_environment`, `ebay_ru_name` columns |
| `channels_location_mappings` | Added `ebay_merchant_location_key` column |

### Key files
| File | Purpose |
|---|---|
| `src/Services/EbayClient.php` | REST client — OAuth + Account API implemented; Inventory/Offers/Fulfillment = Phase 4/7 stubs |
| `src/Contracts/EbayDriverInterface.php` | Full 28-method interface for all eBay API groups |
| `src/Data/EbayTokenSet.php` | Readonly DTO returned by token exchange/refresh |
| `src/Http/Controllers/EbayAuthController.php` | OAuth redirect + callback; syncs business policies on connect |
| `src/Models/EbayToken.php` | Token model — access/refresh tokens encrypted at rest |
| `src/Models/EbayPolicy.php` | Business policy cache |
| `src/Models/EbayConfigurator.php` | Listing template model |
| `src/Models/EbaySkuMapping.php` | Per-SKU eBay listing state |
| `routes/web.php` | eBay OAuth routes: `channels/ebay/{channel}/redirect`, `channels/ebay/callback` |

### Channel model methods
```php
$channel->ebayDriver()          // returns EbayDriverInterface (EbayClient)
$channel->getEbayAuthUrl(...)   // builds OAuth URL via EbayClient
$channel->isEbayConnected()     // checks ebayToken relation for non-null access_token
$channel->ebayToken()           // HasOne EbayToken
$channel->ebayConfigurators()   // HasMany EbayConfigurator
$channel->ebayPolicies()        // HasMany EbayPolicy
$channel->ebaySkuMappings()     // HasMany EbaySkuMapping
```

Note: `$channel->driver()` throws `RuntimeException` for eBay channels — use `ebayDriver()` instead. Shopify-only table actions (test connection, SKU sync) are scoped to `platform === Shopify`.

### EbayClient OAuth flow
1. `getAuthorizationUrl($redirectUri, $state)` — uses `$channel->ebay_ru_name` as `redirect_uri` (not the raw URL)
2. `exchangeCodeForTokens($code, $redirectUri)` — also uses RuName; returns `EbayTokenSet`
3. `refreshAccessToken()` — uses stored refresh token; updates `channels_ebay_tokens.access_token`
4. `getValidAccessToken()` — auto-refreshes if within 5 min of expiry

### EbayClient API base URLs
| Environment | API base | Auth base |
|---|---|---|
| Production | `https://api.ebay.com` | `https://auth.ebay.com` |
| Sandbox | `https://api.sandbox.ebay.com` | `https://auth.sandbox.ebay.com` |

---

## OAuth Scopes requested

```
https://api.ebay.com/oauth/api_scope
https://api.ebay.com/oauth/api_scope/sell.inventory
https://api.ebay.com/oauth/api_scope/sell.inventory.readonly
https://api.ebay.com/oauth/api_scope/sell.account
https://api.ebay.com/oauth/api_scope/sell.account.readonly
https://api.ebay.com/oauth/api_scope/sell.fulfillment
https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly
```

All 7 are in the app's granted scope list. `commerce.taxonomy.readonly` is NOT requested (not granted; taxonomy uses basic scope).

---

## Build Phases

| Phase | Status | Description |
|---|---|---|
| 1 | ✅ Done | Migrations, models, enum, Channel relations |
| 2 | ✅ Done | EbayClient OAuth + Account API, EbayAuthController, routes, ChannelResource eBay section |
| 3 | ✅ Done | EbayConfiguratorResource — tabbed Filament form, live category search via Taxonomy API |
| 4 | ✅ Done | EbayClient Inventory/Offer methods, EbayDescriptionRenderer, EbayBatchCreateJob |
| 5 | ✅ Done | EbayBatchSyncJob, SyncProductToEbay single-product wrapper |
| 6 | ✅ Done | ManageListingsV2 eBay bulk actions — configurator picker modal, Create on eBay, bulk sync |
| 7 | Pending | EbayOrderImporter + SyncEbayOrders |
| 8 | Pending | RefreshEbayTokenJob + scheduler registration |

---

## Final price calculation (pricing engine)

eBay sell prices are set by `plugins/webkul/pricing/src/Services/EbayFeeCalculator.php`, invoked from `PricingEngine`. The engine solves a **circular dependency** algebraically: eBay fees are a percentage of the full buyer price, but the buyer price must include those fees — so you can't compute fees without the price and can't set the price without fees.

### What goes into the final price

```
finalPrice = cost + markup + postage (all ex-VAT)
           + VAT on (cost + markup + postage)
           + FVF % on finalPrice
           + Fixed fee (£0.30 or £0.40)
           + Regulatory Operating Fee % on finalPrice
```

Because FVF and Reg Fee are percentages of `finalPrice` itself, the price is derived as a closed-form expression, not iteratively.

### Fee structure (eBay UK — Vehicle Parts)

| Fee | Rate | Notes |
|---|---|---|
| FVF lower band | 9.5% | On sale total £0–£750 |
| FVF upper band | 3.0% | On portion above £750 |
| Fixed fee (Band B/C) | £0.40 | Per order > £10 |
| Fixed fee (Band A) | £0.30 | Per order ≤ £10 |
| Regulatory Operating Fee | 0.35% | On full sale total |

Configured in Pricing → Channel Profiles → eBay profile. Changes there trigger a full SKU reprice.

### Closed-form algebra (EbayFeeCalculator, Step 3)

```
effectiveLower = pctLower + regPct         (e.g. 0.095 + 0.0035 = 0.0985)
base           = cost + markup + postage   (all ex-VAT)

Band A (price ≤ £10):  P = (base + £0.30) × (1+v) / (1 − effectiveLower × (1+v))
Band B (price ≤ £750): P = (base + £0.40) × (1+v) / (1 − effectiveLower × (1+v))
Band C (price > £750): correction = £750 × (pctLower − pctUpper)
                       effectiveUpper = pctUpper + regPct
                       P = (base + £0.40 + correction) × (1+v) / (1 − effectiveUpper × (1+v))
```

The code tries Band A first; if the result exceeds £10 it moves to Band B; if that exceeds £750 it uses Band C.

### Margin identity

The correct algebra guarantees that the seller nets exactly markup_applied after:
1. eBay deducts its percentage and fixed fees
2. Seller remits output VAT to HMRC (on the full selling price)

```
seller net = P − eBayFees − P×v/(1+v) − cost − postage = markup_applied exactly
```

This also satisfies: **ex-VAT price = cost + markup + postage + all fees (all ex-VAT quantities).**

Margin shown in the Pricing Calculation Log:

```
margin % = markup_applied / final_price × 100
```

A 15% markup on a £5.91 eBay price → 2.4% of selling price. This accounts for channel fees and VAT.

Note: margin in the log is for pricer-managed rows only. Fixed-price rows (no `markup_applied`) show a best-effort fee-based estimate.

### VAT note

eBay charges 20% VAT on its own fees (visible on invoices). This is reclaimable on the VAT return for a VAT-registered seller and is **not included** in the fee calculation. Rates in the Channel Profile are pre-VAT figures from eBay's fee schedule.

---

## Policy sync (Account API)

On first OAuth connect, `EbayAuthController::syncPolicies()` calls:
1. `optInToProgram('SELLING_POLICY_MANAGEMENT')` — idempotent, safe to call if already opted in
2. `getFulfillmentPolicies($marketplaceId)` → stores in `channels_ebay_policies` with `policy_type = 'fulfillment'`
3. `getPaymentPolicies($marketplaceId)` → `policy_type = 'payment'`
4. `getReturnPolicies($marketplaceId)` → `policy_type = 'return'`

Policy ID key differs per type: `fulfillmentPolicyId`, `paymentPolicyId`, `returnPolicyId`.
