Make delivery a consequence of payment, not a task someone performs
- Published on
- • 4 mins read•--- views
Manual digital delivery has a shape you can recognise from across the room. Someone watches the order list, finds a free code in a spreadsheet, pastes it into an email, and crosses it off.
Every property of that system follows from the person in the middle. Delivery takes as long as it takes them to notice. It stops when they sleep. And the same code cannot go out twice only because they are paying attention, which is not a guarantee. It is a hope with a good track record.
Events, not polling
The fix is to hang delivery off the events the shop already emits:
add_action('woocommerce_order_status_changed', [$this, 'handle'], 10, 4);
Now nothing has to be watched. Payment lands, the handler resolves a code, the code attaches to the order. Three in the morning works the same as three in the afternoon.
The reason to prefer events over a cron job that scans for unfulfilled orders is latency and honesty. A scanner has a period, and the period is a delay you are choosing to impose on every customer. An event handler runs when the thing happens. Keep a scanner as a safety net for orders whose event was missed, but do not make it the primary path.
Store state, not a strikethrough
Codes need to live in a table with per-code state, and the reason is not tidiness. Issuing a code and activating it are different facts. A code you handed to a customer who never redeemed it is not the same as one that has been consumed, and a business that cannot tell them apart cannot answer "did this customer actually get what they paid for".
A spreadsheet cell with a line through it collapses both into one bit. Worse, it collapses them destructively: the previous state is gone.
One implementation, two callers
Orders rarely arrive from one place for long. A marketplace channel shows up, and it expects delivery in minutes.
The temptation is to write a second fulfilment path for it, because the marketplace integration lives elsewhere and does not have a WordPress session. Resist that. Expose the same logic over an authenticated API instead:
POST /wp-json/…/activate-product-code Authorization: Bearer <jwt>
The delivery logic then has exactly one implementation with two callers. The alternative, two implementations, means every future change to how delivery works has to be made twice, and the day someone makes it once is the day the two channels start behaving differently. That bug is maddening, because both paths look correct in isolation.
A token rather than an account, incidentally, because handing an admin account to an integration gives it every capability in WordPress to solve a problem that needs one.
Make the external channel queryable
If orders come from a marketplace, the system needs to answer "which orders came from there and what happened to them" as a query. Tag them at creation with their source and their origin id.
Without that, reconciling the two systems becomes a periodic manual chore, which is exactly the work you removed from delivery reappearing in accounting. The pattern repeats: anything you will later need to ask about, record at the moment it happens. Reconstructing it afterwards from timestamps and amounts is guesswork wearing a spreadsheet.
Where this leaves the operator
Automating delivery does not remove people. It changes what they do: instead of performing every delivery, they handle the ones that failed. That is a better use of a person and a much smaller job, but it only works if failures are visible. An automated pipeline that fails silently is worse than a manual one, because nobody is watching the thing that used to be watched by definition.
So the same commit that automates delivery should make failure loud: a note on the order, a status that stands out in the list, a log line with the order id in it.
Related
Projects

CactusPlay - Game shop run by a vetted contractor network
Before: CIS buyers compared PlayStation and Xbox prices by hand across scattered sellers, and every order had to be filled by the founders themselves. After: a shop with a live catalogue and a CRM any vetted contractor with Turkish payment details could join to take orders and earn on them. Result: 20,000+ positions grabbed hourly, price changes reaching the shop and Yandex.Market untouched, and fulfilment no longer capped by one pair of hands.
Posts
Pricing a catalogue against a store you do not control
Reselling means your prices depend on someone else changing theirs. Fetching on page load takes your shop down with theirs; a nightly export goes stale. The middle path is a cache with a pricing strategy behind it.
A support widget that already knows who it is talking to
Pasting a chat widget into a theme template works until the theme updates, the settings need changing, or the widget adds a database query to every page. Three small decisions fix all of it.
Taking payment through a gateway that belongs to another site
When the payment gateway is contracted to a different site, the buyer leaves and you have to learn the outcome somehow. The two ways this integration usually fails are an unauthenticated callback and two order records that drift apart.
Open for contract collaboration
I am available for contract-based collaboration. If you have an interesting project idea, schedule a call via Calendly.
Schedule a 30-min call