OrderLemon is a sales channel, WooCommerce is the system of record
| Data | Direction | Behavior |
| Categories | WooCommerce -> OrderLemon | Synchronized before products |
| Products and variations | WooCommerce -> OrderLemon | One-way only. OrderLemon never sends product updates back to WooCommerce |
| Orders | OrderLemon -> WooCommerce | Imported by webhook or hourly pull |
| Order Status | Bidirectional | Status changes use webhooks, loop prevention, and retry handling |
| Customers | OrderLemon -> WooCommerce | Found or created during order import |
Business rules:
- WooCommerce stock is reduced only when an OrderLemon order is imported
- Products are never hard-deleted in OrderLemon, they are disabled with enabled = 0
- OrderLemon requires a category before a product can be created
- A product synchronized by this plugin must have exactly one WooCommerce product category
- There is no product reverse-sync and no product.stock_updated webhook flow
Runtime requirements:
- PHP 8.3+
- Wordpress 6.9+
- WooCommerce 10.4.3+
- WooCommerce HPOS enabled
The plugin declares HPOS compatibility and remains inactive when HPOS is disabled. Activation checks WordPress, WooCommerce, and WooCommerce versions. Existing stores with legacy orders are sent to WooCommerce's Features screen for migration. HPOS must be enabled. The plugin remains inactive and shows an admin notice when HPOS is disabled. WooCommerce deactivation is handled by WordPress Core through Requires Plugins.
scripts/setup.sh local
docker compose up -dLocal URLs:
- Store: http://localhost:8000
- Admin: http://localhost:8000/wp-admin
- OrderLemon settings: http://localhost:8000/wp-admin/admin.php?page=orderlemon-settings
- phpMyAdmin: http://localhost:8081
Default local credentials:
- WordPress admin: admin`/ admin123
- phpMyAdmin: wordpress / wordpress
These credentials are for local development only. The local Docker environment is not connected to OrderLemon, so it cannot verify live API calls, webhook delivery, archive transitions, or bidirectional end-to-end behavior
Main entry point:
- `orderlemon-woocommerce/orderlemon-woocommerce.php`
| Component | Path | Responsibility |
| Bootstrap | `orderlemon-woocommerce.php` | Requirements, HPOS, activation, tables, initialization, and deactivation |
| Config | `src/Core/Config.php` | Environment URL, connection state, options, and config-file override |
| API client | `src/Core/ApiClient.php` | Native OrderLemon API communication, response validation, and request retries |
| Constants | `src/Core/Constants.php` | Shared HTTP, environment, stock, and order-status constants |
| ErrorCodes | `src/Core/ErrorCodes.php` | Standardized error codes |
| EntityLocator | `src/Core/EntityLocator.php` | WooCommerce order/product lookup and request-level caching |
| TaxSetup | `src/Core/TaxSetup.php` | Tax helpers used by imported orders |
| AtomicLock | src/Core/AtomicOptionLock.php | Database-backed mutex for imports, webhooks, and full sync |
| QR auth | `src/Auth/Auth.php` | Device registration, verification, shop selection, and webhook registration |
| Product sync | `src/Sync/ProductSync.php` | WooCommerce category, product, variation, modifier, and stock sync |
| Order orchestrator | `src/Sync/OrderSync.php` | Delegates order import, status sync, and customer statistics |
| Order import | `src/Sync/Orders/OrderImportHandler.php` | Creates WooCommerce orders from normalized OrderLemon data |
| Order status sync | `src/Sync/Orders/OrderStatusSyncHandler.php` | Sends WooCommerce status changes to OrderLemon |
| Customer stats | `src/Sync/Orders/OrderCustomerStatsHandler.php` | Updates customer statistics after order status changes |
| Payment status | `src/Sync/Orders/PaymentStatusTrait.php` | Shared payment-status resolution |
| Customer manager | `src/Sync/CustomerManager.php` | Customer lookup, creation, privacy hooks, and statistics |
| Webhook endpoint | `src/Webhook/WebhookHandler.php` | REST route, authentication, validation, idempotency, and dispatch |
| Order webhook handler | `src/Webhook/Handlers/OrderWebhookHandler.php` | Order create/update/status/cancel/refund handling |
| Logger | `src/Core/Logger.php` | WooCommerce logs and structured sync logs with redaction |
| Retry Queue | `src/Core/RetryQueue.php` | Persistent asynchronous retry jobs |
| Admin | src/Admin/* and admin/views/* | Settings, logs, notices, meta boxes, status, and rate limiting |
Initialization:
- Database upgrade check: init priority 5
- Main plugin initialization: init priority 10, after WooCommerce
- Admin services load only in wp-admin
- HPOS compatibility is declared on before_woocommerce_init
- Public-facing cart/checkout assets are not currently registered by a Frontend service
| Environment | API URL |
| dev | https://api.orderlemon.dev/v1/ |
| test | https://test.api.orderlemon.dev/v1/ |
| stage | https://stg.api.orderlemon.com/v1/ |
| production | https://api.orderlemon.com/v1/ |
sandbox is unsupported and resolves to production. HTTPS is required for test, stage, and production; plain HTTP is allowed only for dev.
Config precedence:
- Mutable connection state stored in WordPress takes precedence over the config file. This includes auth.token, api.token, company.id, store.id, whatsapp.number, and webhook.secret
- Other values are read from the config file first and the WordPress options
- Config::set() writes to Wordpress options
- api.url is read from the config file only. The orderlemon_api_url WordPress is ignored
- An invalid custom URL falls back to the selected environment URL
General settings:
- orderlemon_sync_modeConnection state:
- orderlemon_auth_token
- orderlemon_api_token (legacy fallback)
- orderlemon_company_id
- orderlemon_store_id
- orderlemon_shop_name
- orderlemon_webhook_secret
- orderlemon_webhook_id
- orderlemon_webhook_url
- orderlemon_connected_at
Immediate API request retry tuning:
- orderlemon_max_retries, default 3
- orderlemon_initial_backoff, default 1 second
- orderlemon_max_backoff, default 60 seconds
orderlemon_excluded_products is only the settings form value. The product post meta orderlemon_sync_enabled is the source of truth for exclusion
Table names must use $wpdb->prefix
| Table | Purpose |
| {$wpdb->prefix}orderlemon_sync_log | Structured sync and webhook logs |
| {$wpdb->prefix}orderlemon_retry_queue | Persistent retry jobs |
| Key | Purpose |
| _orderlemon_product_id | Linked OrderLemon product or modifier ID |
| _orderlemon_last_sync | Last sync timestamp |
| _orderlemon_sync_enabled | Per-product sync toggle |
| _orderlemon_disabled_in_ol | The mapped OL row is disabled |
| _orderlemon_payload_hash | Bulk-sync change-detection hash |
| _orderlemon_relation_key | Confirmed parent/modifier relation |
| _orderlemon_had_variations | Parent variation cleanup marker |
| _orderlemon_synced_category_id | Category used by the last sync |
Category term meta:
- _orderlemon_category_id
- _orderlemon_category_enabled
- _orderlemon_is_modifier
Order meta:
- _orderlemon_order_id
- _orderlemon_logistics_type
- _orderlemon_logistics_label
- _orderlemon_pickup_moment
- _orderlemon_delivery_moment
- _orderlemon_payment_type
User meta:
- _orderlemon_bsuid
- _orderlemon_phone_normalized
- _orderlemon_customer_id
- _orderlemon_whatsapp_customer
Module: products
Authorization: Bearer <token>
Content-Type: application/json
{"a":"update","data":{"company_id":1,"shop_id":2,"product_id":123}}The Module value identifies the domain, for example products, categories, orders, company, login, or webhook. The action is in the a field. ApiClient injects company_id and shop_id when absent and rejects requests when either is missing.
Important product actions:
- products:list — reference-based recovery and duplicate handling.
- products:new — create; category_id is required.
- products:update — update using product_id.
- products:available_modifiers — modifier lookup.
Important order actions:
- orders:list — hourly order pull.
- orders:update — outbound status update.
ApiClient validates JSON, uses SSL verification, handles legal-signature HTTP 420, retries rate limits and retryable 5xx responses, and returns WP_Error for business or transport failures.
ProductSync listens to:
- woocommerce_new_product
- woocommerce_update_product
- woocommerce_product_set_stock
- woocommerce_variation_set_stock
- woocommerce_variation_set_stock_status
- woocommerce_new_product_variation
- woocommerce_update_product_variation
- woocommerce_before_delete_product_variation
- before_delete_post
- wp_trash_post
- transition_post_status for future → publish
The orderlemon_auto_sync_products option is checked first. When it is disabled, product save, variation, stock, and deletion hooks do not send anything to OrderLemon.
Save hooks are coalesced during the request and enqueued at shutdown. API work runs in the persistent retry queue rather than delaying the product-edit request.
A product is eligible only when:
- it loads as WC_Product;
- its status is publish;
- it is not password protected;
- _orderlemon_sync_enabled is not no;
- its product type is allowed by orderlemon_allowed_product_types (default: simple and variable);
- it passes the category sync mode.
Category modes:
- all — all categories are allowed.
- include — only selected categories are allowed.
- exclude — selected categories are excluded.
The full-sync report separates transient skipped results from excluded_product, excluded_category, unsupported_type, generic excluded, disabled, success, and failed results.
| WooCommerce | OrderLemon |
| SKU, or WC-{id} fallback | reference |
| Product ID | metadata.woocommerce.product_id |
| Category ID | metadata.woocommerce.category_id |
| Name | name |
| Description | description |
| Featured image | picture_base64 |
| Regular price | unit_price |
| Sale price or null | sale_price |
| Sale dates | sale_price_start, sale_price_end |
| WC tax rate | vat_percentage |
| Published state | enabled |
| First category | category_id and category_name |
| Stock quantity | stock |
| Manage stock | isstock |
| Menu order | position |
| Featured flag | isfav |
Product images are limited to 20 MiB before Base64 encoding. Oversized local or remote images are skipped with a warning, and product synchronization continues without the image.
Variable products are synchronized as modifier products in OrderLemon:
Variable products are represented by an OL parent product and modifier products for variations. ProductSync creates modifier categories, variation products, parent/modifier relations, and disables stale modifiers.The admin Sync All Products flow first checks for missing SKUs and then starts a background run. State is stored in orderlemon_full_sync_state.
The background process uses:
- one-minute start cooldown;
- start and batch AtomicOptionLock locks;
- 30-minute stale-run takeover protection;
- a stable published-product count and maximum ID snapshot;
- ascending keyset pages of 40 product IDs;
- durable page checkpointing;
- a 20-second batch budget, or 300 seconds when set_time_limit(0) succeeds;
- up to three exception attempts per product;
- run IDs and compare-and-swap state checkpoints;
- stale-product disabling only after a clean product phase.
Products published after the snapshot are processed by a later run. Bulk payload hashes are an optimization only; they do not detect manual changes made directly in OrderLemon. A product Update or orderlemon_full_sync_force_resend can be used to repair remote drift.
Endpoint:
POST /wp-json/orderlemon/v1/webhookThe URL is generated with rest_url(). Plain permalinks can work through the rest_route form. If the site URL or permalink structure changes after connection, the remote registered URL may become invalid; the plugin stores the registered URL and warns about mismatches.
Only these events are registered with OrderLemon:
- payments.completed
- order.updated
The handler retains compatibility dispatch for:
- order.created
- order.status_changed
- order.cancelled
- order.refunded
Compatibility routes must not be mistaken for the current remote event subscription.
Accepted payload shapes:
- Flat: {"event":"order.created","payload":{...}}
- Native: {"orderlemon":{...},"event":{"type":"payments.completed","id":"..."}}
- rejects payloads over 1 MiB;
- validates JSON before processing;
- rate-limits failed attempts by REMOTE_ADDR;
- supports admin whitelist and block controls;
- fails closed when the secret is missing;
- logs only whitelisted identifiers and counts.
For native events with an event ID, the deduplication key is scoped by company ID, shop ID, event type, and event ID. A database-backed lock serializes concurrent delivery. A concurrent request receives HTTP 503 and Retry-After: 5. The one-day completion marker is written only after successful processing, and duplicates return HTTP 200.
Order creation has a separate lock by OrderLemon order ID. Refunds use an order-level lock and a durable external refund ID stored as _orderlemon_refund_id before the refund's first save.
OrderImportHandler::create_from_orderlemon() normalizes the payload and:
1. checks for an existing _orderlemon_order_id;
2. acquires and rechecks an import lock;
3. creates an HPOS-compatible order with created_via=orderlemon;
4. finds or creates the customer;
5. adds product, modifier, shipping, fee, and discount items;
6. applies payment, status, logistics, attribution, and dates;
7. preserves OrderLemon custom prices and VAT totals;
8. saves the order;
9. reduces stock unless the status is cancelled or failed;
10. stores the entity mapping and logs success.
The hourly orderlemon_pull_orders job uses the same import handler and advances orderlemon_last_order_pull only when the pull has no failures.
Product resolution priority:
1. metadata.woocommerce.product_id;
2. product_reference matched to WC SKU;
3. WC-{id} fallback when the current product has no own SKU;
4. explicit wc_product_id.
Unresolved products are retained as OL-only line items rather than silently discarded.
Cart item classification supports regular products, modifiers, campaign products, positive explicit fee lines, and negative discount lines. A fee requires campaign_id to be explicitly present with value 0, no meaningful parent_id, and a positive amount.
Customer lookup priority:
1. BSUID from user_id;
2. normalized phone;
3. email.
BSUID is required for WhatsApp users who adopt usernames and may have an empty phone field. When a phone match receives a new non-conflicting BSUID, the existing customer is linked to it.
New or updated customers use metadata such as:
- _orderlemon_bsuid
- _orderlemon_phone_normalized
- _orderlemon_customer_id
- _orderlemon_whatsapp_customer
Customer data participates in WordPress privacy export/erase and WooCommerce anonymization hooks.
OrderLemon -> WooCommerce
| OL status | WooCommerce |
| ordering | pending |
| ordered | paid |
| done | completed |
| packed | processing |
| readyfordelivery | processing |
| shipped | completed |
| delivered | completed |
| cancelled | cancelled |
| lost | failed |
Cash orders with payment_status=unpaid remain pending instead of becoming paid
paid is a custom public WooCommerce status and is registered as a paid status for WooCommerce reports and analytics
WooCommerce → OrderLemon
| WooCommerce | OrderLemon status ID |
| pending | 1 |
| on-hold | 1 |
| paid | 2 |
| processing | 4 |
| completed | 3 |
| cancelled | 8 |
| refund | 8 |
| failed | 9 |
Loop prevention:
- _ol_incoming_status_{orderlemon_id} marks an OL-originated status transition.
- _ol_status_sync_{orderlemon_id} marks an outbound status update.
Completed, cancelled, failed, and refunded OL-created orders may already be archived. A later WooCommerce change away from those terminal states is logged and not sent to the active-order API. Transport failures enqueue order_status_update; business failures are not retried.
Payment types:
| OL value | WooCommerce method |
| 1 or cash | cod |
| 2 or online | orderlemon_online |
| 3 or b2b | orderlemon_b2b |
The COD title is read from the WooCommerce gateway configuration. Other payment titles can be customized with orderlemon_payment_method_title.
Admin surfaces:
- WooCommerce → OrderLemon
- WooCommerce → OrderLemon Logs
- WooCommerce → OL Rate Limiting
- WooCommerce → Status, including the OrderLemon diagnostics section
- Product, category, and order meta boxes
Admin functionality includes QR connection, disconnect, environment selection, API test, support bundle download, rate-limit controls, product exclusion, modifier selection, order metadata, and background full-sync progress.
Cron hooks:
| Hook | Schedule | Purpose |
| orderlemon_pull_orders | Hourly | Pull OrderLemon orders |
| orderlemon_cleanup_logs | Daily | Clean sync logs and terminal retry jobs |
| orderlemon_process_retry_queue | Every 5 minutes | Process due retry jobs |
| orderlemon_retry_queue_kick | One-shot | Process newly queued jobs immediately |
| orderlemon_full_sync_batch | Chained one-shot | Process full-sync pages |
| orderlemon_cleanup_webhook_events | Expiry-driven one-shot | Remove completed webhook markers |
Jobs are deduplicated atomically. Retryable failures use exponential backoff and five attempts by default. Deterministic business errors fail immediately. Completed and failed jobs are cleaned up after seven days.
The logger writes to the WooCommerce logger and the sync-log table. Sensitive values, authorization headers, customer data, addresses, notes, secrets, and base64 images are redacted or summarized.
| Action | Arguments |
| orderlemon_webhook_processing | none |
| orderlemon_webhook_order_created | int order_id, array payload |
| orderlemon_webhook_order_updated | int order_id, array payload |
| orderlemon_webhook_order_status_changed | int order_id, string new_status, array payload |
| orderlemon_webhook_order_cancelled | int order_id, array payload |
| orderlemon_webhook_order_refunded | int order_id, int refund_id, array payload |
| orderlemon_customer_created | int user_id, array customer_data |
| orderlemon_disconnected | none |
Confirmed custom filters:
| Filter | Arguments |
| orderlemon_config_path | string config_path |
| orderlemon_allowed_product_types | array types |
| orderlemon_prepare_product_data | array data, WC_Product product |
| orderlemon_product_reference | string reference, WC_Product product |
| orderlemon_default_vat_percentage | float default |
| orderlemon_payment_method_title | string default, context array |
| orderlemon_full_sync_force_resend | bool force, int product_id |
The orderlemon_product_reference filter is identity-critical. Any custom reference format must be tested across product sync, disable cleanup, duplicate handling, reimport recovery, and incoming cart resolution.