← Yuriy Chukaev · PortfolioCase 02
Case 02 · Case study

A pharmacy chain, fully online

I migrated a 13-store, two-country pharmacy chain onto a custom PHP 8.3 / MySQL commerce engine, with the hard part being a resilient integration surface: idempotent two-way sync with two pharmacy ERPs, payment routing across six legal entities, lawful fiscalization, delivery, and messenger bots.

PHP 8.3MySQLTwo-way ERP syncIdempotency & de-dupPayment routing (6 entities)FiscalizationMulti-region catalogRegulated products

A regional pharmacy chain — 13 stores spread across two countries and operated under six legal entities — needed to sell online for real: live per-store stock, local pricing, regulated-product rules, and lawful fiscal receipts issued by the correct entity. Their boxed CMS could not model any of that. I rebuilt the storefront on a custom PHP 8.3 / MySQL engine, but the engineering that mattered was the integration surface: two-way sync with two different pharmacy ERPs, payment routing per legal entity, fiscalization, a delivery API, and messenger bots — all made idempotent so nothing double-charges, double-ships, or double-syncs.

The problem

Pharmacy retail is not generic e-commerce. Stock is per-store and moves in real time; the same SKU has different prices in different regions; some products are regulated and cannot be sold or displayed the same way everywhere; and every sale must produce a lawful fiscal receipt issued by the legal entity that actually owns that store. The chain ran thirteen stores across two countries under six legal entities, and each store's inventory and prices lived inside a pharmacy ERP — two different ERP systems across the group, not one. A boxed CMS treats the catalog as a single flat list with one price and one seller. It had no concept of per-store stock, per-region pricing, per-entity billing, or fiscalization, so the chain effectively could not sell online in a way that was correct or lawful.

The scope

14,000+
products with live per-store stock
6
regional storefronts
2
pharmacy ERPs synced two-way
6
legal entities for billing & receipts

Architecture

I built the engine as a commerce core wrapped by an integration layer, so external systems never touch domain logic directly — every ERP, payment provider, fiscal service and delivery API sits behind an adapter with its own retry, idempotency and reconciliation rules.

Commerce core (PHP 8.3 / MySQL)

Catalog, cart, checkout and order lifecycle. Products, stock and prices are modeled per store and per region rather than as flat globals, so the same SKU carries different availability and pricing depending on which storefront the customer is on.

ERP sync layer

One adapter per ERP normalizing two vendor dialects into a single internal contract for stock, price and orders. Sync is two-way — inbound stock/price from the ERP, outbound orders back to it — with de-duplication and per-field conflict resolution so the two systems converge instead of fighting.

Payment routing & fiscalization

Each order is keyed to the legal entity that owns its store, then routed to that entity's payment credentials and its fiscal channel, so money lands in the right account and the receipt is issued lawfully by the correct seller across all six entities.

Regional catalog & pricing

A region/pricing layer over the catalog resolves price and availability per storefront, and applies regulated-product rules — controlling how restricted items are displayed, sold, and handled — differently per country to satisfy each jurisdiction.

Delivery integration

A delivery-service adapter wires fulfillment end to end: it hands off shipments, tracks state back into the order lifecycle, and is idempotent on retries so a re-sent request never creates a duplicate shipment.

Messenger bots

Bot channels sit on the same order and status model as the web storefronts, giving customers order and delivery updates through messengers without a separate parallel data path.

Key decisions & trade-offs

Decision — Make ERP sync idempotent, with de-duplication and per-field source-of-truth

Two ERPs feeding one storefront will contradict each other, and the same update can arrive twice. I made every sync operation idempotent, keyed to stable external identifiers, and defined conflict resolution per field: stock and price are authoritative from the ERP, order state is authoritative from the commerce core, so each field has exactly one owner. The rejected alternative was last-write-wins, which is simpler but silently corrupts inventory the moment a retry or a slow message reorders events. The trade-off I accepted is more schema and mapping work up front (a de-dup index and a documented ownership rule for every synced field) in exchange for a system that converges deterministically instead of drifting.

Decision — Pull-based reconciliation instead of trusting ERP webhooks

The ERPs could emit webhooks, but in practice they were unreliable — dropped, delayed, or fired out of order — and a missed stock event on a pharmacy catalog means overselling a regulated product. I chose scheduled pull-based reconciliation as the source of truth: the engine periodically reads authoritative state from each ERP and reconciles, using webhooks only as an optional low-latency hint. The rejected alternative, a purely event-driven pipeline, is cheaper on read volume and lower latency but is only as correct as the least reliable webhook. The trade-off I accepted is deliberately higher read volume against the ERPs in exchange for correctness and self-healing — because reconciliation is idempotent, a missed event is simply caught on the next pass instead of becoming a permanent discrepancy.

Decision — Route payment and fiscalization by legal entity, not by storefront

It would have been easy to bind billing to the storefront a customer was browsing, but six legal entities own the thirteen stores, and the receipt must be issued by the entity that actually sells the item. I keyed payment routing and fiscalization to the owning legal entity of the order's store, so the charge hits that entity's payment account and the fiscal receipt is issued lawfully under that entity. The rejected alternative — one merchant account with reconciliation done later in accounting — is far simpler to build but produces unlawful receipts and a manual money-splitting problem every single day. The trade-off is a more complex checkout that has to resolve the entity before it can charge, in exchange for money and receipts that are correct at the moment of sale.

Decision — A per-region catalog/pricing layer with explicit regulated-product handling

Rather than fork the catalog per storefront or hard-code a second-country variant, I built one catalog with a region/pricing resolution layer on top. Price, availability and regulated-product rules are resolved per region at read time. That made the second-country launch a matter of configuring a region — localized pricing plus that jurisdiction's regulated-product rules — rather than standing up a new store. The rejected alternative of separate per-country deployments would have duplicated the catalog and doubled the maintenance surface; the trade-off is a more abstract data model in exchange for launching new regions without cloning the system.

Stack

LanguagePHP 8.3
DatabaseMySQL
IntegrationsTwo pharmacy ERPs (two-way), multi-entity payments, fiscalization service, delivery API, messenger bots
Sync modelIdempotent operations, de-duplication, per-field source-of-truth, pull-based reconciliation
CatalogPer-store stock, per-region pricing, regulated-product rules
BillingPayment routing and fiscal receipts keyed to legal entity

Outcome

The chain now sells online across six regional storefronts in two countries with live per-store stock and pricing, payments routed to the correct legal entity, lawful fiscal receipts, and delivery wired end to end. Because the sync is idempotent and reconciliation is pull-based, inventory stays consistent with the ERPs without manual intervention, and launching a new region is a configuration step rather than a rebuild — which is exactly how the second-country launch was done.