# Reverb OpenCart Integration An OpenCart extension that syncs products, stock, prices, orders, and images between an OpenCart store and a Reverb.com marketplace listing. ## Project Overview **Type:** OpenCart 3.x Extension (with planned OpenCart 4.x compatibility) **Primary Market:** Australia (reverb.com/au) **Sync Direction:** Bidirectional (OpenCart ↔ Reverb) ## Reference Documentation - [Reverb Developer Integrations](https://reverb.com/au/page/integrations) - [Reverb Help: Developer Sections](https://help.reverb.com/hc/en-us/sections/40908931289883) - [OpenCart Extension Development Guide](https://github.com/opencart/opencart/wiki/OpenCart-Extension-Development-Guide) - Reverb API Base URL: `https://api.reverb.com/api/` - Auth: `Authorization: Bearer ` + `Accept-Version: 3.0` headers ## Requirements ### Admin Settings Page The extension must provide an admin panel (`Admin > Extensions > Modules > Reverb`) that allows the store owner to: - Enter and save their personal Reverb.com API token - Select sync direction: One-way (OpenCart → Reverb) or Both-ways (bidirectional) - Select which OpenCart categories are eligible for sync - Manually trigger a full catalogue sync - Manually trigger an order import from Reverb ### Product Page Integration Each product in the OpenCart admin must show a toggle/checkbox to: - Enable or disable syncing that individual product to Reverb - Set the item condition (Mint, Excellent, Good, etc.) - Set a per-product Reverb category override (or leave blank to use the category mapping default) - Mark the item as handmade - Declare whether the item has a UPC/EAN or is exempt - Show the Reverb listing ID and a direct link once the product is listed ### Sync Scope (Bidirectional) | Field | OC → Reverb | Reverb → OC | |--------------|:-----------:|:-----------:| | Title / Name | ✓ | ✓ | | Description | ✓ | ✓ | | Price | ✓ | ✓ | | Stock / Qty | ✓ | ✓ | | Images | ✓ | — | | Orders | — | ✓ | ## Extension Architecture (OpenCart 3.x) ### File Structure ``` upload/ ├── admin/ │ ├── controller/extension/module/ │ │ └── reverb.php # Admin settings, manual sync, order import, AJAX endpoints │ ├── language/en-gb/extension/module/ │ │ └── reverb.php # All user-facing strings │ ├── model/extension/module/ │ │ └── reverb.php # DB interactions, sync logic, order import │ └── view/template/extension/module/ │ ├── reverb.twig # Admin settings form (tabs: Settings, Category Mapping, Sync Log) │ └── reverb_product.twig # Per-product Reverb tab (loaded via AJAX) ├── catalog/ │ └── controller/extension/module/ │ └── reverb.php # Webhook receiver + cron polling endpoint └── system/ └── library/reverb/ ├── ReverbApi.php # HTTP client wrapping the Reverb REST API ├── ProductMapper.php # Maps OC product fields ↔ Reverb listing fields └── OrderMapper.php # Maps Reverb order payload → OC order format ``` OCMOD patch file (`install.xml`) injects the per-product Reverb tab into the product edit page without modifying core files. The `upload/` folder and `install.xml` are zipped into `reverb.ocmod.zip` for installation via Extensions > Installer in the OpenCart admin backend. ### Database Tables The extension creates and manages the following tables. **`oc_reverb_product_map`** | Column | Type | Notes | |------------------------|--------------|----------------------------------------------| | `product_id` | INT | FK to `oc_product` | | `reverb_listing_id` | VARCHAR(64) | Reverb's listing ID once synced | | `sync_enabled` | TINYINT(1) | Per-product on/off toggle (default 0) | | `condition_uuid` | VARCHAR(64) | Reverb condition UUID | | `reverb_category_uuid` | VARCHAR(64) | Reverb category UUID (overrides mapping) | | `handmade` | TINYINT(1) | Whether the item is handmade (default 0) | | `upc_does_not_apply` | TINYINT(1) | Whether item is UPC/EAN exempt (default 1) | | `last_synced_at` | DATETIME | Timestamp of last successful sync | **`oc_reverb_order_map`** | Column | Type | Notes | |-----------------------|--------------|----------------------------------------------| | `reverb_order_number` | VARCHAR(64) | Primary key; Reverb order number | | `order_id` | INT | Corresponding OC order ID | | `reverb_status` | VARCHAR(32) | Reverb order status at time of import | | `created_at` | DATETIME | When the order was imported | **`oc_reverb_sync_log`** | Column | Type | Notes | |--------------|---------------------------|-------------------------| | `log_id` | INT AUTO_INCREMENT | | | `product_id` | INT | | | `direction` | ENUM('push','pull') | | | `status` | ENUM('success','error') | | | `message` | TEXT | Error detail or summary | | `created_at` | DATETIME | | API token, sync mode, and category whitelist are stored in OpenCart's native `oc_setting` table via `$this->config` — no custom config table needed. ### Key Design Decisions 1. **API Token storage:** Stored via OpenCart's native settings system (`oc_setting`) with the key prefix `module_reverb_`. Never stored in a raw custom table. 2. **Per-product toggle:** Stored in `oc_reverb_product_map.sync_enabled`; defaults to `0` (disabled) for all products. 3. **Category filter:** Admin selects OpenCart categories; only products in those categories AND with `sync_enabled = 1` are synced. 4. **Webhooks vs polling:** Use Reverb webhooks for real-time order/listing updates; fall back to a scheduled cURL ping (OpenCart cron or system cron calling `catalog/controller/extension/module/reverb/cron`) for stock/price polling. 5. **Image sync:** Product images are submitted as a flat array of absolute URL strings inside the `PUT /listings/{id}` payload (`{"photos": ["https://..."], "publish": true}`). There is no separate photo upload endpoint in Reverb API v3. Images are pushed OC → Reverb only; never pulled back. The store URL is read from `config_url` with fallback to `HTTPS_CATALOG` / `HTTP_CATALOG` constants from `admin/config.php`. 6. **Order deduplication:** Imported orders are tracked in `oc_reverb_order_map` keyed by `reverb_order_number`. Both the admin import action and the catalog webhook handler check this table before creating a new OC order. 7. **Category hierarchy:** Reverb categories are fetched from `/categories/flat` and cached in `oc_setting` for 24 hours. The `full_name` field (e.g. `"Guitars > Electric Guitars"`) is parsed to group categories by root name for `` rendering in dropdowns. 8. **String encoding:** OC stores product names and descriptions with HTML entities and tags. `ProductMapper::toReverb()` applies `html_entity_decode()` to the title and converts HTML markup to plain text for the description before sending to Reverb. ## Reverb API Essentials Base URL: `https://api.reverb.com/api/` Required headers on every request: ``` Authorization: Bearer Accept-Version: 3.0 Content-Type: application/hal+json ``` Rate limit: ~100 requests/min on the standard plan — batch operations where possible. Key endpoints: | Endpoint | Method | Purpose | | --- | --- | --- | | `/my/listings` | GET | List all existing seller listings | | `/listings` | POST | Create a new listing | | `/listings/{id}` | PUT | Update listing (also accepts `photos` URL array and `publish: true`) | | `/listings/{id}/end` | PUT | End/delist a listing | | `/my/orders/selling/all` | GET | Fetch all seller orders (supports `updated_start_date`, `page`, `per_page`) | | `/my/orders/selling/{order_number}` | GET | Fetch a single order | | `/webhooks` | POST | Register a webhook endpoint | | `/categories/flat` | GET | Get the full Reverb category tree (each entry has `uuid`, `full_name`, `name`) | | `/listing_conditions` | GET | Get available item conditions | When creating API token in Reverb these are the available scopes: | Scope | Permission | | --- | --- | | public | Read publicly available data | | read_listings | Read all of your listings with sales and bump data | | write_listings | Create/update listings (inventory, price, etc) | | read_orders | Read all your orders | | write_orders | Update the status of your orders | | read_profile | Get account and shop details | | write_profile | Update shop settings | | read_feedback | Read feedback sent or received | | write_feedback | Write feedback about transactions | | read_messages | Retrieve messages | | write_messages | Post and update messages | | read_offers | Read offers | | write_offers | Make offers on listings | | read_reviews | Read listing reviews | | write_reviews | Write listing reviews | | read_lists | Read watch list / feed | | write_lists | Update watch list / feed | | read_payouts | Read payout data | | read_addresses / write_addresses | Read / write addresses | ## Sync Flow ### OpenCart → Reverb (triggered on product save event or manual sync) 1. Check product has `sync_enabled = 1` AND is in an allowed category. 2. Map OC product fields to a Reverb listing payload via `ProductMapper::toReverb()`: - Title: `html_entity_decode()` applied - Description: br/p tags → newlines, `strip_tags()`, `html_entity_decode()` - Includes `make` (manufacturer), `handmade`, `upc_does_not_apply`, condition, category, shipping rates 3. Collect product images; build absolute URLs using store URL. Append `photos` array and `publish: true` to the payload. 4. If `reverb_listing_id` exists → `PUT /listings/{id}`; otherwise → `POST /listings` and store the returned ID. 5. Update `last_synced_at` and log result in `oc_reverb_sync_log`. ### Reverb → OpenCart: Order Import (admin manual trigger or cron) 1. Admin clicks "Import Orders from Reverb" → `extension/module/reverb/importOrders` (AJAX). 2. Model calls `importOrdersFromReverb()`: paginates `GET /my/orders/selling/all` with optional `updated_start_date` from last import. 3. Skips already-imported orders (check `oc_reverb_order_map`) and non-actionable statuses (cancelled, unpaid, etc.). 4. Directly inserts into `oc_order`, `oc_order_product`, `oc_order_total`, `oc_order_history`. Decrements OC product stock. 5. Saves to `oc_reverb_order_map`. Updates `module_reverb_order_last_sync` setting. ### Reverb → OpenCart: Webhooks / Cron 1. Receive webhook POST to `catalog/controller/extension/module/reverb/webhook` (or cron polls listings + orders). 2. For `listing/update`: look up OC product by `reverb_listing_id`, update price/stock/description. 3. For `order/create`: check `oc_reverb_order_map` for duplicates, map via `OrderMapper::toOpenCart()`, create via `model_checkout_order->addOrder()`, save to `oc_reverb_order_map`. 4. Log result. ## OpenCart 4.x Compatibility Notes - OC 4.x uses PHP namespaces: `namespace Opencart\Admin\Controller\Extension\Module;` - Controllers must extend `\Opencart\System\Engine\Controller` - Template paths and module structure differ from 3.x - Planned approach: ship separate 4.x controller files; share `system/library/reverb/` classes between both versions (they are framework-agnostic) ## Development Guidelines - PHP 7.4+ required; PHP 8.1+ preferred - Use `$this->db->query()` and `$this->db->escape()` for all database access — never raw PDO or unsanitised input - All user-visible strings must be defined in the language file, not hardcoded in controllers or templates - Wrap all Reverb API calls in try/catch; log errors to `oc_reverb_sync_log` rather than surfacing raw exceptions to the UI - The OCMOD XML file is required for injecting the per-product toggle without modifying core OpenCart files - Schema migrations run via `migrate()` called from `saveProductMap()` and `getProductMap()` — uses `information_schema` to add columns only if missing, safe to run repeatedly - Test against a Reverb sandbox account before connecting a live store