From 9afdeaa008993618be351a06715ef2199331d12f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:11:13 +0000 Subject: [PATCH 01/24] Initial plan From b0eb14b009af5064aedd79960733df2a9224ad64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:16:32 +0000 Subject: [PATCH 02/24] Add eBay store category hierarchy support (3-level mapping) Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- .../migrate-add-ebay-store-categories.php | 56 ++++++ database/schema.sql | 12 +- database/schema.sqlite.sql | 10 + src/models/Product.php | 183 +++++++++++++++++- 4 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 database/migrate-add-ebay-store-categories.php diff --git a/database/migrate-add-ebay-store-categories.php b/database/migrate-add-ebay-store-categories.php new file mode 100644 index 00000000..bb8abc00 --- /dev/null +++ b/database/migrate-add-ebay-store-categories.php @@ -0,0 +1,56 @@ +getConnection(); + + echo "Adding eBay store category columns to products table...\n"; + + // Check if columns already exist + $columns = [ + 'ebay_store_cat1_id' => 'INT NULL', + 'ebay_store_cat1_name' => 'VARCHAR(255) NULL', + 'ebay_store_cat2_id' => 'INT NULL', + 'ebay_store_cat2_name' => 'VARCHAR(255) NULL', + 'ebay_store_cat3_id' => 'INT NULL', + 'ebay_store_cat3_name' => 'VARCHAR(255) NULL' + ]; + + $addedColumns = []; + foreach ($columns as $columnName => $columnDef) { + try { + // Try to select the column to see if it exists + $db->query("SELECT $columnName FROM products LIMIT 1"); + echo "Column '$columnName' already exists. Skipping...\n"; + } catch (PDOException $e) { + // Column doesn't exist, add it + echo "Adding column '$columnName'...\n"; + $db->exec("ALTER TABLE products ADD COLUMN $columnName $columnDef"); + $addedColumns[] = $columnName; + } + } + + if (empty($addedColumns)) { + echo "All eBay store category columns already exist. No changes needed.\n"; + } else { + echo "\nSuccessfully added columns: " . implode(', ', $addedColumns) . "\n"; + } + + echo "\nMigration completed successfully!\n"; + echo "Products can now store exact 3-level eBay store category hierarchy.\n"; + echo "\nColumn structure:\n"; + echo "- ebay_store_cat1_id / ebay_store_cat1_name: Level 1 (main category: Motorcycle, ATV, Boat, etc.)\n"; + echo "- ebay_store_cat2_id / ebay_store_cat2_name: Level 2 (manufacturer/subcategory)\n"; + echo "- ebay_store_cat3_id / ebay_store_cat3_name: Level 3 (model/sub-subcategory)\n"; + +} catch (Exception $e) { + echo "Migration failed: " . $e->getMessage() . "\n"; + exit(1); +} diff --git a/database/schema.sql b/database/schema.sql index bbe828f6..c6e6e53c 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -21,6 +21,13 @@ CREATE TABLE IF NOT EXISTS products ( source VARCHAR(20) DEFAULT 'manual', -- 'ebay' or 'manual' show_on_website BOOLEAN DEFAULT TRUE, -- TRUE = visible, FALSE = hidden is_active BOOLEAN DEFAULT TRUE, + -- eBay store category hierarchy (exact 3-level mapping) + ebay_store_cat1_id INT, + ebay_store_cat1_name VARCHAR(255), + ebay_store_cat2_id INT, + ebay_store_cat2_name VARCHAR(255), + ebay_store_cat3_id INT, + ebay_store_cat3_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category), @@ -29,7 +36,10 @@ CREATE TABLE IF NOT EXISTS products ( INDEX idx_is_active (is_active), INDEX idx_manufacturer (manufacturer), INDEX idx_source (source), - INDEX idx_show_on_website (show_on_website) + INDEX idx_show_on_website (show_on_website), + INDEX idx_ebay_store_cat1 (ebay_store_cat1_id), + INDEX idx_ebay_store_cat2 (ebay_store_cat2_id), + INDEX idx_ebay_store_cat3 (ebay_store_cat3_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Categories table diff --git a/database/schema.sqlite.sql b/database/schema.sqlite.sql index da52cc02..9679006e 100644 --- a/database/schema.sqlite.sql +++ b/database/schema.sqlite.sql @@ -27,6 +27,13 @@ CREATE TABLE IF NOT EXISTS products ( show_on_website INTEGER DEFAULT 1, -- 1 = visible, 0 = hidden warehouse_id INTEGER, is_active INTEGER DEFAULT 1, + -- eBay store category hierarchy (exact 3-level mapping) + ebay_store_cat1_id INTEGER, + ebay_store_cat1_name TEXT, + ebay_store_cat2_id INTEGER, + ebay_store_cat2_name TEXT, + ebay_store_cat3_id INTEGER, + ebay_store_cat3_name TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) @@ -39,6 +46,9 @@ CREATE INDEX IF NOT EXISTS idx_products_is_active ON products(is_active); CREATE INDEX IF NOT EXISTS idx_products_manufacturer ON products(manufacturer); CREATE INDEX IF NOT EXISTS idx_products_source ON products(source); CREATE INDEX IF NOT EXISTS idx_products_show_on_website ON products(show_on_website); +CREATE INDEX IF NOT EXISTS idx_products_ebay_store_cat1 ON products(ebay_store_cat1_id); +CREATE INDEX IF NOT EXISTS idx_products_ebay_store_cat2 ON products(ebay_store_cat2_id); +CREATE INDEX IF NOT EXISTS idx_products_ebay_store_cat3 ON products(ebay_store_cat3_id); -- Categories table CREATE TABLE IF NOT EXISTS categories ( diff --git a/src/models/Product.php b/src/models/Product.php index 695b77f3..c26f3eed 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -228,8 +228,9 @@ public function create($data) { $sql = "INSERT INTO products ( ebay_item_id, sku, name, description, price, sale_price, quantity, category, - manufacturer, model, condition_name, weight, length, width, height, image_url, images, ebay_url, source, show_on_website - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + manufacturer, model, condition_name, weight, length, width, height, image_url, images, ebay_url, source, show_on_website, + ebay_store_cat1_id, ebay_store_cat1_name, ebay_store_cat2_id, ebay_store_cat2_name, ebay_store_cat3_id, ebay_store_cat3_name + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->db->prepare($sql); $result = $stmt->execute([ @@ -252,7 +253,13 @@ public function create($data) isset($data['images']) ? json_encode($data['images']) : null, $data['ebay_url'] ?? null, $data['source'] ?? 'manual', - isset($data['show_on_website']) ? $data['show_on_website'] : 1 + isset($data['show_on_website']) ? $data['show_on_website'] : 1, + $data['ebay_store_cat1_id'] ?? null, + $data['ebay_store_cat1_name'] ?? null, + $data['ebay_store_cat2_id'] ?? null, + $data['ebay_store_cat2_name'] ?? null, + $data['ebay_store_cat3_id'] ?? null, + $data['ebay_store_cat3_name'] ?? null ]); if ($result) { @@ -273,7 +280,9 @@ public function update($id, $data) $allowedFields = [ 'sku', 'name', 'description', 'price', 'sale_price', 'quantity', 'category', 'manufacturer', 'model', 'condition_name', 'weight', 'length', 'width', 'height', - 'image_url', 'images', 'ebay_url', 'source', 'show_on_website' + 'image_url', 'images', 'ebay_url', 'source', 'show_on_website', + 'ebay_store_cat1_id', 'ebay_store_cat1_name', 'ebay_store_cat2_id', 'ebay_store_cat2_name', + 'ebay_store_cat3_id', 'ebay_store_cat3_name' ]; foreach ($allowedFields as $field) { @@ -354,6 +363,105 @@ private function mapEbayCategory($ebayCategoryName, $ebayCategoryId, $itemTitle) return $category; } + /** + * Extract full 3-level eBay store category hierarchy + * Returns array with category IDs and names for all 3 levels + * + * @param int|null $storeCategoryId Primary store category ID from eBay + * @param int|null $storeCategory2Id Secondary store category ID from eBay + * @param \FAS\Integrations\EbayAPI|null $ebayAPI EbayAPI instance + * @return array Array with cat1_id, cat1_name, cat2_id, cat2_name, cat3_id, cat3_name + */ + private function extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2Id, $ebayAPI) + { + $hierarchy = [ + 'cat1_id' => null, + 'cat1_name' => null, + 'cat2_id' => null, + 'cat2_name' => null, + 'cat3_id' => null, + 'cat3_name' => null + ]; + + if (!$ebayAPI || !$storeCategoryId) { + return $hierarchy; + } + + // Get all store categories + $storeCategories = $ebayAPI->getStoreCategories(); + if (empty($storeCategories)) { + return $hierarchy; + } + + // Look up the category by ID + if (!isset($storeCategories[$storeCategoryId])) { + // Try secondary category if primary not found + if ($storeCategory2Id && isset($storeCategories[$storeCategory2Id])) { + $storeCategoryId = $storeCategory2Id; + } else { + return $hierarchy; + } + } + + $category = $storeCategories[$storeCategoryId]; + $level = $category['level'] ?? 0; + + // Based on the level, extract the hierarchy + if ($level == 1) { + // Level 1: Only top-level category + $hierarchy['cat1_id'] = $storeCategoryId; + $hierarchy['cat1_name'] = $category['name']; + } elseif ($level == 2) { + // Level 2: Top-level + manufacturer + $hierarchy['cat2_id'] = $storeCategoryId; + $hierarchy['cat2_name'] = $category['name']; + + // Find parent (level 1) by looking for topLevel + $topLevelName = $category['topLevel'] ?? null; + if ($topLevelName) { + foreach ($storeCategories as $catId => $cat) { + if ($cat['level'] == 1 && $cat['name'] === $topLevelName) { + $hierarchy['cat1_id'] = $catId; + $hierarchy['cat1_name'] = $cat['name']; + break; + } + } + } + } elseif ($level == 3) { + // Level 3: Complete hierarchy + $hierarchy['cat3_id'] = $storeCategoryId; + $hierarchy['cat3_name'] = $category['name']; + + // Get parent (level 2) name + $parentName = $category['parent'] ?? null; + $topLevelName = $category['topLevel'] ?? null; + + // Find level 2 parent + if ($parentName) { + foreach ($storeCategories as $catId => $cat) { + if ($cat['level'] == 2 && $cat['name'] === $parentName && $cat['topLevel'] === $topLevelName) { + $hierarchy['cat2_id'] = $catId; + $hierarchy['cat2_name'] = $cat['name']; + break; + } + } + } + + // Find level 1 (top-level) + if ($topLevelName) { + foreach ($storeCategories as $catId => $cat) { + if ($cat['level'] == 1 && $cat['name'] === $topLevelName) { + $hierarchy['cat1_id'] = $catId; + $hierarchy['cat1_name'] = $cat['name']; + break; + } + } + } + } + + return $hierarchy; + } + /** * Sync product from eBay data * @param array $ebayData Product data from eBay API @@ -449,6 +557,13 @@ public function syncFromEbay($ebayData, $ebayAPI = null) $sku = $ebayData['id']; } + // Extract full 3-level eBay store category hierarchy + $storeCategoryHierarchy = $this->extractStoreCategoryHierarchy( + $ebayData['store_category_id'] ?? null, + $ebayData['store_category2_id'] ?? null, + $ebayAPI + ); + // Priority 2: Extract category, manufacturer and model from store categories (ONLY as fallback) $storeCategoryFound = false; if ($ebayAPI && isset($ebayData['store_category_id']) && $ebayData['store_category_id']) { @@ -500,7 +615,14 @@ public function syncFromEbay($ebayData, $ebayAPI = null) 'image_url' => $image, 'images' => $images, 'ebay_url' => $ebayData['url'] ?? null, - 'source' => 'ebay' + 'source' => 'ebay', + // Store exact eBay store category hierarchy (all 3 levels) + 'ebay_store_cat1_id' => $storeCategoryHierarchy['cat1_id'], + 'ebay_store_cat1_name' => $storeCategoryHierarchy['cat1_name'], + 'ebay_store_cat2_id' => $storeCategoryHierarchy['cat2_id'], + 'ebay_store_cat2_name' => $storeCategoryHierarchy['cat2_name'], + 'ebay_store_cat3_id' => $storeCategoryHierarchy['cat3_id'], + 'ebay_store_cat3_name' => $storeCategoryHierarchy['cat3_name'] ]; // Check if product has required shipping dimensions/weight @@ -511,6 +633,9 @@ public function syncFromEbay($ebayData, $ebayAPI = null) // Don't update category on sync - preserve admin's setting unset($productData['category']); + // ALWAYS update eBay store category hierarchy - this must stay in sync with eBay + // These fields represent the exact eBay store structure and should always reflect current state + // Always update manufacturer if we have Brand from eBay (most reliable source) // Only preserve existing manufacturer if we don't have Brand from eBay if (empty($ebayData['brand']) && !empty($existing['manufacturer'])) { @@ -563,4 +688,52 @@ public function updateImages($prodId, $newImagesJson) { $stmt->bindParam(':pid', $prodId); return $stmt->execute(); } + + /** + * Get full eBay store category path for a product + * Returns formatted string like "Motorcycle > Honda > CR500" + * + * @param array $product Product data array + * @return string|null Category path or null if no eBay categories + */ + public function getEbayStoreCategoryPath($product) + { + $path = []; + + if (!empty($product['ebay_store_cat1_name'])) { + $path[] = $product['ebay_store_cat1_name']; + } + if (!empty($product['ebay_store_cat2_name'])) { + $path[] = $product['ebay_store_cat2_name']; + } + if (!empty($product['ebay_store_cat3_name'])) { + $path[] = $product['ebay_store_cat3_name']; + } + + return !empty($path) ? implode(' > ', $path) : null; + } + + /** + * Get eBay store categories as an array + * Returns array with level => name mapping + * + * @param array $product Product data array + * @return array Array of categories [1 => 'Level 1', 2 => 'Level 2', 3 => 'Level 3'] + */ + public function getEbayStoreCategoryArray($product) + { + $categories = []; + + if (!empty($product['ebay_store_cat1_name'])) { + $categories[1] = $product['ebay_store_cat1_name']; + } + if (!empty($product['ebay_store_cat2_name'])) { + $categories[2] = $product['ebay_store_cat2_name']; + } + if (!empty($product['ebay_store_cat3_name'])) { + $categories[3] = $product['ebay_store_cat3_name']; + } + + return $categories; + } } From e68db83a96d2ba87c8d86e71eca0c96d5df34117 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:23:23 +0000 Subject: [PATCH 03/24] Display eBay store category hierarchy in product pages Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- admin/products.php | 27 ++++++++++++++++++++++++++- product.php | 10 ++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/admin/products.php b/admin/products.php index f187ba7e..dab558c7 100644 --- a/admin/products.php +++ b/admin/products.php @@ -378,7 +378,17 @@ - eBay + getEbayStoreCategoryPath($prod); + ?> + + title="" + data-bs-toggle="tooltip" + data-bs-placement="top" + > + eBay + Manual @@ -556,6 +566,21 @@ + + + getEbayStoreCategoryArray($product); ?> + +
+ + eBay Store Categories:
+ $catName): ?> + L:
+ + Synced from eBay store +
+
+ +
diff --git a/product.php b/product.php index e079c0f4..d038a8b4 100644 --- a/product.php +++ b/product.php @@ -159,6 +159,16 @@ class="img-thumbnail thumbnail-image + getEbayStoreCategoryPath($product); + if (!empty($ebayCategory)): + ?> + + eBay Category: + + + Manufacturer: From b1250ba254bef1307f0d5b125b7b4ae05c42f52b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:25:40 +0000 Subject: [PATCH 04/24] Address code review: optimize performance and fix security issues Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- .../migrate-add-ebay-store-categories.php | 36 ++++- docs/EBAY_CATEGORY_MAPPING.md | 141 ++++++++++++++++++ src/models/Product.php | 44 +++--- 3 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 docs/EBAY_CATEGORY_MAPPING.md diff --git a/database/migrate-add-ebay-store-categories.php b/database/migrate-add-ebay-store-categories.php index bb8abc00..e3fba15b 100644 --- a/database/migrate-add-ebay-store-categories.php +++ b/database/migrate-add-ebay-store-categories.php @@ -23,14 +23,40 @@ 'ebay_store_cat3_name' => 'VARCHAR(255) NULL' ]; + // Whitelist of allowed column names for security + $allowedColumns = [ + 'ebay_store_cat1_id', + 'ebay_store_cat1_name', + 'ebay_store_cat2_id', + 'ebay_store_cat2_name', + 'ebay_store_cat3_id', + 'ebay_store_cat3_name' + ]; + $addedColumns = []; foreach ($columns as $columnName => $columnDef) { - try { - // Try to select the column to see if it exists - $db->query("SELECT $columnName FROM products LIMIT 1"); + // Security check: only allow whitelisted column names + if (!in_array($columnName, $allowedColumns, true)) { + echo "Column '$columnName' is not in the whitelist. Skipping for security...\n"; + continue; + } + + // Check if column exists using PRAGMA for SQLite + $result = $db->query("PRAGMA table_info(products)"); + $existingColumns = $result->fetchAll(PDO::FETCH_ASSOC); + + $columnExists = false; + foreach ($existingColumns as $col) { + if ($col['name'] === $columnName) { + $columnExists = true; + break; + } + } + + if ($columnExists) { echo "Column '$columnName' already exists. Skipping...\n"; - } catch (PDOException $e) { - // Column doesn't exist, add it + } else { + // Column doesn't exist, add it - safe because columnName is whitelisted echo "Adding column '$columnName'...\n"; $db->exec("ALTER TABLE products ADD COLUMN $columnName $columnDef"); $addedColumns[] = $columnName; diff --git a/docs/EBAY_CATEGORY_MAPPING.md b/docs/EBAY_CATEGORY_MAPPING.md new file mode 100644 index 00000000..5d6dca06 --- /dev/null +++ b/docs/EBAY_CATEGORY_MAPPING.md @@ -0,0 +1,141 @@ +# eBay Store Category Mapping + +## Overview + +This document describes how the FAS e-commerce platform synchronizes and maintains exact 3-level category hierarchy from eBay store to ensure perfect alignment between the website and eBay store categories. + +## Architecture + +### Database Schema + +Products now store the complete eBay store category hierarchy with 6 new columns: + +- `ebay_store_cat1_id` (INT) - Level 1 category ID +- `ebay_store_cat1_name` (VARCHAR) - Level 1 category name (e.g., "MOTORCYCLE") +- `ebay_store_cat2_id` (INT) - Level 2 category ID +- `ebay_store_cat2_name` (VARCHAR) - Level 2 category name (e.g., "Honda") +- `ebay_store_cat3_id` (INT) - Level 3 category ID +- `ebay_store_cat3_name` (VARCHAR) - Level 3 category name (e.g., "CR500") + +All columns are indexed for efficient querying. + +### Category Hierarchy + +eBay stores support a 3-level category structure: + +1. **Level 1 (Top Level)**: Main product category (Motorcycle, ATV, Boat, etc.) +2. **Level 2 (Manufacturer)**: Product manufacturer or subcategory +3. **Level 3 (Model)**: Specific model or sub-subcategory + +### Synchronization Process + +When products are synced from eBay: + +1. The eBay API fetches store category IDs for each product +2. The full category hierarchy is extracted using `getStoreCategories()` +3. All 3 levels are stored in the database +4. The hierarchy is preserved exactly as defined in the eBay store + +## Implementation Details + +### Product Model Methods + +#### `extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2Id, $ebayAPI)` +Extracts the complete 3-level hierarchy from eBay store categories. + +**Returns:** +```php +[ + 'cat1_id' => int|null, + 'cat1_name' => string|null, + 'cat2_id' => int|null, + 'cat2_name' => string|null, + 'cat3_id' => int|null, + 'cat3_name' => string|null +] +``` + +#### `getEbayStoreCategoryPath($product)` +Returns a formatted category path string. + +**Example:** +```php +"MOTORCYCLE > Honda > CR500" +``` + +#### `getEbayStoreCategoryArray($product)` +Returns categories organized by level. + +**Example:** +```php +[ + 1 => 'MOTORCYCLE', + 2 => 'Honda', + 3 => 'CR500' +] +``` + +### Sync Behavior + +**For New Products:** +- All 6 eBay store category columns are populated +- The `category` field is set based on the mapped website category +- Products maintain exact eBay store classification + +**For Existing Products:** +- eBay store category columns are ALWAYS updated to match current eBay store structure +- The `category` field is preserved (admin override) +- This ensures eBay store changes are reflected while respecting admin customizations + +## Display + +### Product Detail Page +Shows the full eBay store category path in the product details section: +``` +eBay Category: MOTORCYCLE > Honda > CR500 +``` + +### Admin Product List +Hover over the "eBay" badge to see the full category path in a tooltip. + +### Admin Product Edit +For products synced from eBay, displays the full category hierarchy with badges: +- L1: MOTORCYCLE +- L2: Honda +- L3: CR500 + +## Migration + +To add category support to an existing database: + +```bash +php database/migrate-add-ebay-store-categories.php +``` + +This migration: +- Adds all 6 new columns +- Creates indexes +- Is idempotent (safe to run multiple times) + +## Benefits + +1. **Exact Synchronization**: Website categories perfectly match eBay store structure +2. **Data Preservation**: Products maintain their exact eBay classification +3. **Automatic Updates**: Category changes in eBay store are reflected on next sync +4. **No Data Loss**: All 3 levels preserved, no information discarded +5. **Efficient Queries**: Indexed columns enable fast category-based searches + +## Future Enhancements + +Potential improvements: +- Filter products by eBay store categories on website +- Display category breadcrumbs on product listings +- Category-based product recommendations +- Store category analytics and reporting + +## Notes + +- The `category` field (motorcycle, atv, boat, etc.) remains for website navigation +- eBay store categories are for reference and sync accuracy +- Products without eBay categories (manual products) have null values in these columns +- The hierarchy supports partial categorization (1, 2, or 3 levels) diff --git a/src/models/Product.php b/src/models/Product.php index c26f3eed..d911a6bc 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -387,7 +387,7 @@ private function extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2 return $hierarchy; } - // Get all store categories + // Get all store categories (cached in EbayAPI) $storeCategories = $ebayAPI->getStoreCategories(); if (empty($storeCategories)) { return $hierarchy; @@ -406,6 +406,16 @@ private function extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2 $category = $storeCategories[$storeCategoryId]; $level = $category['level'] ?? 0; + // Build indexed lookups to avoid O(n) searches + // Group categories by level and name for fast lookup + static $levelIndex = []; + if (empty($levelIndex)) { + foreach ($storeCategories as $catId => $cat) { + $levelKey = $cat['level'] . ':' . $cat['name'] . ':' . ($cat['topLevel'] ?? ''); + $levelIndex[$levelKey] = ['id' => $catId, 'name' => $cat['name']]; + } + } + // Based on the level, extract the hierarchy if ($level == 1) { // Level 1: Only top-level category @@ -419,12 +429,10 @@ private function extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2 // Find parent (level 1) by looking for topLevel $topLevelName = $category['topLevel'] ?? null; if ($topLevelName) { - foreach ($storeCategories as $catId => $cat) { - if ($cat['level'] == 1 && $cat['name'] === $topLevelName) { - $hierarchy['cat1_id'] = $catId; - $hierarchy['cat1_name'] = $cat['name']; - break; - } + $key = '1:' . $topLevelName . ':' . $topLevelName; + if (isset($levelIndex[$key])) { + $hierarchy['cat1_id'] = $levelIndex[$key]['id']; + $hierarchy['cat1_name'] = $levelIndex[$key]['name']; } } } elseif ($level == 3) { @@ -437,24 +445,20 @@ private function extractStoreCategoryHierarchy($storeCategoryId, $storeCategory2 $topLevelName = $category['topLevel'] ?? null; // Find level 2 parent - if ($parentName) { - foreach ($storeCategories as $catId => $cat) { - if ($cat['level'] == 2 && $cat['name'] === $parentName && $cat['topLevel'] === $topLevelName) { - $hierarchy['cat2_id'] = $catId; - $hierarchy['cat2_name'] = $cat['name']; - break; - } + if ($parentName && $topLevelName) { + $key = '2:' . $parentName . ':' . $topLevelName; + if (isset($levelIndex[$key])) { + $hierarchy['cat2_id'] = $levelIndex[$key]['id']; + $hierarchy['cat2_name'] = $levelIndex[$key]['name']; } } // Find level 1 (top-level) if ($topLevelName) { - foreach ($storeCategories as $catId => $cat) { - if ($cat['level'] == 1 && $cat['name'] === $topLevelName) { - $hierarchy['cat1_id'] = $catId; - $hierarchy['cat1_name'] = $cat['name']; - break; - } + $key = '1:' . $topLevelName . ':' . $topLevelName; + if (isset($levelIndex[$key])) { + $hierarchy['cat1_id'] = $levelIndex[$key]['id']; + $hierarchy['cat1_name'] = $levelIndex[$key]['name']; } } } From 01ce4a16c91672cbc46fec01bbe726bd9529dfee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:34:52 +0000 Subject: [PATCH 05/24] Improve eBay API error handling for better diagnostics Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- src/integrations/EbayAPI.php | 38 +++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/integrations/EbayAPI.php b/src/integrations/EbayAPI.php index ff8aae12..9442def6 100644 --- a/src/integrations/EbayAPI.php +++ b/src/integrations/EbayAPI.php @@ -547,12 +547,40 @@ private function makeTradingApiRequest($url, $xmlRequest, $retryCount = 0, $maxR // Check for API errors if (isset($data['Ack']) && ($data['Ack'] === 'Failure' || $data['Ack'] === 'PartialFailure')) { - $errorMsg = $data['Errors']['LongMessage'] ?? $data['Errors']['ShortMessage'] ?? 'Unknown error'; - error_log('eBay Trading API Error: ' . $errorMsg); - SyncLogger::logError('Trading API returned error: ' . $errorMsg); + // Extract error message(s) - handle both single error and array of errors + $errorMsg = 'Unknown error'; + $errorCode = null; - // Check for rate limit - if (isset($data['Errors']['ErrorCode']) && $data['Errors']['ErrorCode'] == '21919300') { + if (isset($data['Errors'])) { + $errors = $data['Errors']; + + // Handle array of errors (multiple errors) + if (isset($errors[0])) { + $firstError = $errors[0]; + $errorMsg = $firstError['LongMessage'] ?? $firstError['ShortMessage'] ?? 'Unknown error'; + $errorCode = $firstError['ErrorCode'] ?? null; + + // If multiple errors, include count + if (count($errors) > 1) { + $errorMsg .= ' (and ' . (count($errors) - 1) . ' more errors)'; + } + } else { + // Single error object + $errorMsg = $errors['LongMessage'] ?? $errors['ShortMessage'] ?? 'Unknown error'; + $errorCode = $errors['ErrorCode'] ?? null; + } + + // If still unknown, log the full error structure for debugging + if ($errorMsg === 'Unknown error') { + SyncLogger::log('DEBUG - Full error structure: ' . print_r($errors, true)); + } + } + + error_log('eBay Trading API Error: ' . $errorMsg . ($errorCode ? " (Code: $errorCode)" : '')); + SyncLogger::logError('Trading API returned error: ' . $errorMsg . ($errorCode ? " (Code: $errorCode)" : '')); + + // Check for rate limit (error code 21919300) + if ($errorCode == '21919300') { if ($retryCount < $maxRetries) { $waitTime = self::RATE_LIMIT_BASE_WAIT * pow(self::RATE_LIMIT_MULTIPLIER, $retryCount); SyncLogger::log("Rate limit hit. Retrying {$retryCount}/{$maxRetries} after {$waitTime} seconds"); From 4b9ea7122041c28df51cb2ee611a727d2fe25c4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:35:36 +0000 Subject: [PATCH 06/24] Use strict comparison for error code checking Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- src/integrations/EbayAPI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/EbayAPI.php b/src/integrations/EbayAPI.php index 9442def6..dc99dc70 100644 --- a/src/integrations/EbayAPI.php +++ b/src/integrations/EbayAPI.php @@ -580,7 +580,7 @@ private function makeTradingApiRequest($url, $xmlRequest, $retryCount = 0, $maxR SyncLogger::logError('Trading API returned error: ' . $errorMsg . ($errorCode ? " (Code: $errorCode)" : '')); // Check for rate limit (error code 21919300) - if ($errorCode == '21919300') { + if ($errorCode === '21919300') { if ($retryCount < $maxRetries) { $waitTime = self::RATE_LIMIT_BASE_WAIT * pow(self::RATE_LIMIT_MULTIPLIER, $retryCount); SyncLogger::log("Rate limit hit. Retrying {$retryCount}/{$maxRetries} after {$waitTime} seconds"); From ebe5bc6ff0520906c91ae3e65decb8d12fcd6110 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:53:43 +0000 Subject: [PATCH 07/24] Add migration guide documentation Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- database/README_MIGRATION.md | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 database/README_MIGRATION.md diff --git a/database/README_MIGRATION.md b/database/README_MIGRATION.md new file mode 100644 index 00000000..06e6767e --- /dev/null +++ b/database/README_MIGRATION.md @@ -0,0 +1,110 @@ +# Database Migration Guide + +## eBay Store Category Columns Migration + +This migration adds support for storing the complete 3-level eBay store category hierarchy in the products table. + +### What This Migration Does + +Adds 6 new columns to the `products` table: +- `ebay_store_cat1_id` - Level 1 category ID (e.g., ID for "MOTORCYCLE") +- `ebay_store_cat1_name` - Level 1 category name (e.g., "MOTORCYCLE") +- `ebay_store_cat2_id` - Level 2 category ID (e.g., ID for "Honda") +- `ebay_store_cat2_name` - Level 2 category name (e.g., "Honda") +- `ebay_store_cat3_id` - Level 3 category ID (e.g., ID for "CR500") +- `ebay_store_cat3_name` - Level 3 category name (e.g., "CR500") + +### How to Run the Migration + +**IMPORTANT:** You must run this migration before syncing products, otherwise you'll get an error like: +``` +SQLSTATE[HY000]: General error: 1 table products has no column named ebay_store_cat1_id +``` + +#### Step 1: Navigate to the project directory +```bash +cd /path/to/FAS +``` + +#### Step 2: Run the migration script +```bash +php database/migrate-add-ebay-store-categories.php +``` + +#### Step 3: Verify the migration +You should see output like: +``` +Adding eBay store category columns to products table... +Adding column 'ebay_store_cat1_id'... +Adding column 'ebay_store_cat1_name'... +Adding column 'ebay_store_cat2_id'... +Adding column 'ebay_store_cat2_name'... +Adding column 'ebay_store_cat3_id'... +Adding column 'ebay_store_cat3_name'... + +Successfully added columns: ebay_store_cat1_id, ebay_store_cat1_name, ebay_store_cat2_id, ebay_store_cat2_name, ebay_store_cat3_id, ebay_store_cat3_name + +Migration completed successfully! +``` + +If the columns already exist, you'll see: +``` +All eBay store category columns already exist. No changes needed. +``` + +### After Running the Migration + +Once the migration completes successfully, you can: +1. Run eBay sync normally - products will now store the full 3-level category hierarchy +2. View product details pages to see the complete category path (e.g., "MOTORCYCLE > Honda > CR500") +3. Check admin product listings to see category information in tooltips + +### Troubleshooting + +**Error: Config file not found** +- Make sure you have `src/config/config.php` set up +- Copy `src/config/config.example.php` to `src/config/config.php` if needed + +**Error: Database not found** +- Initialize the database first using `php database/init-sqlite.php` + +**Error: Permission denied** +- Ensure the database file has write permissions +- Check that the database directory is writable + +**Already ran but getting errors?** +- The migration is idempotent (safe to run multiple times) +- It will skip columns that already exist +- If you see the error again after running successfully, check your database connection + +### Manual Migration (Alternative) + +If the automated script doesn't work, you can add the columns manually: + +**For SQLite:** +```sql +ALTER TABLE products ADD COLUMN ebay_store_cat1_id INTEGER; +ALTER TABLE products ADD COLUMN ebay_store_cat1_name TEXT; +ALTER TABLE products ADD COLUMN ebay_store_cat2_id INTEGER; +ALTER TABLE products ADD COLUMN ebay_store_cat2_name TEXT; +ALTER TABLE products ADD COLUMN ebay_store_cat3_id INTEGER; +ALTER TABLE products ADD COLUMN ebay_store_cat3_name TEXT; +``` + +**For MySQL:** +```sql +ALTER TABLE products ADD COLUMN ebay_store_cat1_id INT NULL; +ALTER TABLE products ADD COLUMN ebay_store_cat1_name VARCHAR(255) NULL; +ALTER TABLE products ADD COLUMN ebay_store_cat2_id INT NULL; +ALTER TABLE products ADD COLUMN ebay_store_cat2_name VARCHAR(255) NULL; +ALTER TABLE products ADD COLUMN ebay_store_cat3_id INT NULL; +ALTER TABLE products ADD COLUMN ebay_store_cat3_name VARCHAR(255) NULL; +``` + +### Support + +If you encounter issues: +1. Check the error message carefully +2. Verify your database is accessible +3. Ensure PHP has permissions to modify the database +4. Review the migration script at `database/migrate-add-ebay-store-categories.php` From 76f0eaee21980536b0e1bd9d46c9a198cd7cfa3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:07:41 +0000 Subject: [PATCH 08/24] Implement dynamic eBay category navigation with 3-tier sidebar - Remove hard-coded categories from navigation menus - Add getStoreCategoriesHierarchical() method to EbayAPI for hierarchical category structure - Update Product model with getAllByEbayCategory() and getCountByEbayCategory() for filtering - Replace products.php with sidebar showing eBay store categories (3 levels) - Support filtering by eBay category IDs (cat1, cat2, cat3) - Display full category path in product cards - Remove category dropdown from header navigation (replaced with simple Products link) Co-authored-by: Stage4000 <46226385+Stage4000@users.noreply.github.com> --- includes/header.php | 16 +- index.php | 16 +- products.php | 532 ++++++++++++++++++++--------------- products.php.backup | 310 ++++++++++++++++++++ products_old.php | 310 ++++++++++++++++++++ src/integrations/EbayAPI.php | 77 +++++ src/models/Product.php | 85 ++++++ 7 files changed, 1090 insertions(+), 256 deletions(-) create mode 100644 products.php.backup create mode 100644 products_old.php diff --git a/includes/header.php b/includes/header.php index 2afc9650..5b8be77e 100644 --- a/includes/header.php +++ b/includes/header.php @@ -94,20 +94,8 @@ - -