CLAUDE.md 13 KB

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

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 <optgroup> 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 <token>
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