WP Manifestindependent plugin directory
manifest / ecommerce / woo-stock-sync

Woo Stock Sync

WooCommerce plugin that syncs stock and prices from supplier feeds with a dry-run diff, batched apply, partial-failure isolation, and rollback.

by Ali Raza · github.com/thealirazadev/woo-stock-sync · website

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/thealirazadev/woo-stock-sync/archive/refs/heads/main.zip

A WooCommerce plugin that keeps store stock levels and prices in sync with a supplier feed. A store owner points the plugin at a CSV upload or a remote CSV/JSON URL, maps feed columns to product fields keyed by SKU, optionally marks supplier prices up by a percentage or a fixed amount, previews every change as a dry-run diff, applies the sync in background batches through Action Scheduler, and can roll back the last applied sync from stored prior values. Every run, row result, and rollback is recorded in custom audit tables, and the same actions are available from WP-CLI.

Status: implemented (v1.0.0)

Screenshots

Dry-run diff preview for a sync run, listing per-field stock and price changes with planned, no-change, skipped (locked and unknown SKU), and failed rows, plus Apply sync and Cancel actions

Settings screen showing the remote feed URL, a masked auth header value, the fetch schedule, and the column mapping of feed columns to SKU, stock, regular price, and sale price

Run history table listing previous sync runs with date, trigger, source, status, and planned, skipped, failed, and applied counts

Stack

  • PHP 7.4+ WordPress plugin (procedural bootstrap + small single-responsibility classes).
  • WordPress 6.0+ and WooCommerce 7.0+; product writes go through WooCommerce CRUD APIs only.
  • Action Scheduler (bundled with WooCommerce) for scheduled fetches and batched apply/rollback.
  • Custom database tables (versioned dbDelta migrations) for sync runs, row results, and snapshots.
  • Vanilla JavaScript and plain CSS for the admin screens; no build step.
  • Dev tooling: Composer, PHP_CodeSniffer with WordPress Coding Standards, PHPUnit with the WordPress test suite.

Design decisions

The trade-offs that shaped v1, with the alternatives that were considered and rejected. Full rationale lives in docs/architecture.md and docs/PRD.md.

  • Custom tables over post meta. Runs, per-row results, and snapshots live in three narrow InnoDB tables, not post meta. A 10,000-row feed synced daily would otherwise write millions of meta rows with no efficient way to list runs, page results, filter by status, or prune history. Run and row results are not attributes of any post, and history has to stay queryable and cleanable. The only post meta used is _wss_locked, which genuinely is a product attribute.
  • WooCommerce CRUD over direct SQL for product writes. Every product change goes through wc_get_product / setters / save(). Direct SQL was rejected because it would bypass object and lookup-table caches (wc_product_meta_lookup), stock-status transitions, and third-party hooks, leaving the catalog subtly inconsistent. Direct SQL is used only for the plugin's own audit tables.
  • Action Scheduler batches sized for shared hosting. Apply and rollback run in Action Scheduler actions of 25 rows (filterable via wss_batch_size), not one long synchronous request. Each row costs a product load plus a CRUD save (~10-20 queries plus hooks); 25 rows finish in a few seconds, comfortably inside Action Scheduler's ~30s budget and a typical shared-host max_execution_time. A giant synchronous apply, a bigger batch, or raw WP-Cron were rejected as timeout-prone on the exact hosting this tool targets; Action Scheduler also ships with WooCommerce (no new dependency), persists jobs, and retries.
  • Dry-run before apply as the default workflow. Every sync starts as a preview: a diff of product, field, current value, new value, and planned action. Nothing is written until the run is explicitly applied. Applying straight from the feed was rejected - the target user needs to see what will change before it changes. Scheduled runs stop at the preview too; auto-apply is opt-in.
  • Update-only in v1. The plugin only updates products that already exist, matched by SKU. Unknown SKUs are reported (unknown_sku), never created. Product creation from a feed pulls in titles, images, categories, and tax/shipping decisions that belong to a PIM, not a stock syncer, so it is deliberately out of scope for v1.
  • Price markup applied at staging, not on write. An optional markup (a percentage or a fixed amount) is applied to the regular and sale prices once, as feed rows are staged, so the adjusted value is what the dry-run diff shows and what the apply later writes through WooCommerce CRUD. Applying the markup separately at diff time and again at apply time was rejected: settings could change between preview and apply, making the preview a lie. Staging the adjusted value once keeps the preview truthful, leaves rollback (which snapshots each product's real prior values) untouched, and never marks up stock. A markup of zero is a byte-identical pass-through, and any result below zero is clamped to 0 so a markdown can never write a negative price.

Install

  1. Copy the plugin folder to wp-content/plugins/woo-stock-sync (or install the built zip through the Plugins screen). WooCommerce must be active first.
  2. Activate it, then open WooCommerce > Stock Sync > Settings to configure a feed source and mapping.

For development:

composer install

Run

Use WooCommerce > Stock Sync in the admin, or the WP-CLI commands:

wp wss fetch [--source=<url>]        # create a run, fetch, stage, and diff
wp wss dry-run [--run=<id>] [--status=<status>] [--format=table|json]
wp wss apply --run=<id> [--yes]      # apply (or resume) a previewed run
wp wss rollback [--yes]              # roll back the most recent applied run
wp wss runs [--format=table|json]    # list run history

Test

composer run lint                    # PHP_CodeSniffer (WordPress standard)
composer run test                    # PHPUnit

Pure-logic tests (feed parsing/validation, the sync lock) run standalone; the integration tests self-skip until WordPress is available. To run the whole suite, point WP_TESTS_DIR at a WordPress core test library and WC_PLUGIN_PATH at a WooCommerce checkout - docs/testing.md has the exact provisioning commands, and CI runs them on every push. wp-env works too but is not required.

Benchmark

The CSV parser and per-row validator (WSS_Feed::parse) run without WordPress, so their throughput is measurable in isolation. The scripts below generate a deterministic synthetic feed and stream it through the parser with a no-op sink, so the timing reflects parsing plus mapping/validation only (no database, no product I/O):

php bin/generate-feed.php 50000 /tmp/feed.csv
php bin/benchmark-parse.php /tmp/feed.csv 21     # median of 21 runs

Measured on a 12th Gen Intel Core i5-1235U, PHP 8.2.29 CLI (no OPcache/JIT), single process, warm page cache, median of the runs shown:

Feed rows File size Parse + validate (median) Throughput Peak PHP memory
50,000 (the enforced row cap) 1.1 MB 0.12 s ~432,000 rows/s 8.5 MB
250,000 5.5 MB 0.58 s ~429,000 rows/s 29 MB
1,000,000 22.2 MB 2.40 s ~417,000 rows/s 102 MB

At the enforced 50,000-row cap a feed parses and validates in well under a second using ~8.5 MB. The file itself is streamed row by row (fgetcsv), so the read buffer is constant; peak memory grows with row count only because duplicate-SKU detection keeps a seen-SKU set for the run, which the row cap bounds to a few megabytes in normal operation. Numbers are hardware- and load-dependent; re-run the scripts to reproduce them on your own machine.

Build

composer run build                   # wp dist-archive -> build/woo-stock-sync.zip

Documentation

Planning docs live in docs/: PRD.md (requirements), architecture.md (design and data model), rules.md (engineering rules), phases.md (build plan), design.md (admin UI), testing.md (test strategy), api-contracts.md (admin-ajax and WP-CLI contracts), memory.md (work log), launch-checklist.md (release gate).