Erpy for Craft CMS

Syncing

Every inbound sync — the control panel button, the queue job, the scheduled run, the console command, a webhook — ends in the same place. That is not tidiness for its own sake: it is what makes paging, delta watermarks, the identity map, dry runs, dead letters and the run log behave identically no matter how a sync was started.

Running one

Erpy → Connections → your connection → Sync now, or:

php craft erpy/sync/status               # where every connection has got to
php craft erpy/sync/run acme             # everything this connection syncs
php craft erpy/sync/run acme product     # one entity
php craft erpy/sync/run acme product --full
php craft erpy/sync/due                  # the one to put on cron
php craft erpy/sync/reset acme product   # forget the watermark; next run is a full one

For a large catalogue, run erpy/sync/due from cron rather than relying on the queue. Cron has no request timeout.

*/5 * * * * cd /path/to/site && php craft erpy/sync/due >> /dev/null 2>&1

Dry runs

Preview reads a page and reports exactly what it would create, update or skip — and changes nothing. Do this first, every time, on a connection you have just changed.

It is the same code path as a real run with the writes withheld, so what it reports is what would happen, not an estimate of it.

Delta watermarks

After a successful run, Erpy remembers where it got to, and the next run asks the ERP only for what changed since.

The watermark advances to the start of a run, never to the end, plus a two-minute overlap. A record modified while a run is in flight would otherwise fall into the gap between "when I started asking" and "when I finished", and never be seen again. Re-reading a few records costs one content-hash comparison; missing one costs a wrong price for as long as nobody notices.

A failed run does not move the watermark at all.

Change detection is by content hash, so a nightly 40,000-SKU pull costs one comparison per unchanged record rather than one element save. That is also why recordSkippedItems is off by default — on a delta sync, "unchanged" is almost every row.

The identity map

Every pairing between an ERP record and a Craft or Commerce element is stored, with a unique index on (connection, entity, natural key).

That index is the duplicate-order guarantee — not a remembered check, not a flag on the order. A retried queue job cannot become a second sales order in your warehouse, because the database will not let it.

Shipment rows carry a quantity, so "has this line been fulfilled?" is answerable across several partial deliveries rather than only the last one.

Nothing in Erpy's tables has a foreign key to a Commerce or Craft element except a user's B2B account. Elements get deleted, and losing a sync history because somebody tidied up a product is worse than holding an id that no longer resolves.

Problems: dead letters and replay

Erpy → Problems.

A document the ERP refused is kept whole, with the request, the response and the reason. Fix the cause and press Retry: the original goes again — rebuilt from Commerce if the order still exists, from the stored copy if it does not.

php craft erpy/orders/retry acme    # resend everything on the Problems screen

A refusal the connector marked as permanent is not retried automatically. One marked retryable is, up to pushMaxAttempts, and then it dead-letters for good rather than queueing forever.

The log

Erpy → Log is every request and response, with credentials redacted at the transport — so there is no path a connector could take that would miss it, including the error bodies that echo your API key back at you.

Turn logBodies off on a busy catalogue if the table grows faster than you want, or logErrorsOnly on. A log write is never allowed to be the reason a sync fails.

Webhooks

A connector that supports them exposes a webhook URL on its connection. A webhook does not do anything different from a scheduled run — it starts the same sync, scoped to what the ERP said changed. That is deliberate: a webhook path with its own write logic is a second implementation that drifts.