Amirali YaghoutiSenior Software Engineer

platform Case study

Pre-owned Watch Marketplace

A pre-owned watch is not a catalogue product. Someone else owns it, a technician has to say what it is, and a single piece sells for a very large sum. So I built the marketplace inside a watch and jewellery retailer's store as a trust system first: every piece is in exactly one state, and nothing the seller types reaches the public without two people signing off.

The business problem

The retailer wanted customers to sell their watches through the store. The naive build is a product form with a 'used' flag, and it fails on the first dispute: a seller's description goes live unchecked, a fake is listed next to genuine stock, a buyer pays for a piece that is still in the seller's drawer, and nobody can say who held the watch when. Submission, inspection, approval, listing, reservation, custody and settlement are different states with different owners, and treating them as one CRUD row makes every one of them unsafe.

There was also a host problem. The store runs on WooCommerce and the certified bank gateways are WooCommerce plugins, so payment had to pass through the shop. But a marketplace sale is not a shop sale: it must not appear in the shop's reports, must not send the shop's emails, and, as I learned, must not touch the customer's real shopping cart on the way through.

What I delivered

  • A five-step seller route: submit, bench inspection by a repair technician, operator approval of photos, reference and identity, seller KYC, then listing. There is no path from the seller's form to a public page that skips the bench or the operator.
  • Custody and delivery as an explicit state machine. Submission, review, certification, listing, reservation, custody and delivery are separate states, transitions are validated, and a piece is in exactly one of them at any moment. The bench's verdict is recorded separately from the current status, because the two are different facts.
  • A bench refusal that returns the file to the seller, with reason labels and the technician's note, instead of parking it in the operator queue. Each reason has a short SMS form because the gateway's pattern tokens cap at thirty characters.
  • Six checkout and payment paths through a WooCommerce bridge, with marketplace orders excluded from shop reports and analytics, shop transactional emails suppressed, and fee-only orders never claiming stock. A piece is reserved only against a confirmed payment, and the callback that confirms it is idempotent, so a retried gateway response cannot reserve it twice.
  • A repair-panel view of the marketplace: waiting, approved and refused groups, fed by the submissions the bench has decided, so the technician sees their own verdicts without leaving their tool.
  • Loyalty earning at a tenth of the store's rate, with four selectable rules (seller on sale, seller on settlement, buyer on purchase, and a flat grant on accepted consignment, off by default) and a configurable seller rank on settled net amount, shown beside the club tier in the account panel.

Technical approach

  • I modelled the states before the payments. The public showcase repository is built the same way: state design, trust boundaries and failure modes first, UI volume later. A strict state machine costs flexibility, and that is the point; it removes the unsafe transitions instead of documenting them.
  • Identity is borrowed from the host storefront; authorization is not. A correct OTP proves who the visitor is, then the module attaches its own buyer or seller role and resolves the right dashboard. Each verification attempt is reserved and each correct code consumed as an atomic, generation-bound transition, the shared object cache is never the source of truth for a multi-request flow, and if the durable rate-limit store cannot reserve a slot, the SMS provider is never called.
  • The refusal route came out of a live failure: the first real refusal sent the seller two messages and a cut-off reason. The rule it produced is one fact, one message, and every reason now exists in a form that fits the token.
  • Points never touch the ledger directly. Every source, repairs, marketplace and counter, goes through one reward service under a per-customer lock, and the idempotency key names the job or the listing, never the moment, so a replayed event cannot pay twice. The marketplace stores Toman, the repair module stores Rial and the club counts Toman, so the conversion lives in one place rather than in every caller.
  • Full separation from WooCommerce was agreed and deliberately deferred: the certified bank gateways are WooCommerce plugins, so the bridge stays until after public launch, and the interference it caused is already fixed.

Result and evidence

No performance figure is claimed here, and the public repository claims none either; what is verified is the route itself. The seller route has been exercised in production, its first live refusal reached the seller with the reason, and the fixes it exposed have shipped. The invariants hold: a piece is in one state at a time, a reservation needs a confirmed payment, a fee-only order never claims stock, and a marketplace sale leaves the shop's reports, emails and carts alone.

Commercial value

A retailer that can take a customer's watch on consignment, certify it and sell it keeps a customer who would otherwise go to a private buyer, and does so without a fake ending up next to its own stock. The state machine is what makes the operator's queue trustworthy and a dispute answerable: who held the piece, who approved the listing and what was paid for it are recorded facts, not recollections.

implementation-brief.readme

Readable implementation brief

implementation_brief {
  project: "Pre-owned Watch Marketplace"
  host: "WordPress/WooCommerce store of a watch and jewellery retailer"
  seller_route: "submit -> bench inspection -> operator approval
                 (photos, reference, identity) -> KYC -> listing"
  states: "submission / review / certification / listing /
           reservation / custody / delivery; one state per piece"
  refusal: "returns to the seller with reason labels and the
            technician's note, never to the operator queue"
  payments: "six paths over a WooCommerce bridge; reservation only
             on confirmed payment; callbacks idempotent"
  isolation: "no shop reports, emails or cart; fee-only orders
              claim no stock"
  loyalty: "one reward service, per-customer lock; marketplace
            earns at a tenth of the store rate; seller rank on
            settled net amount"
  status: "seller route live; no performance KPI claimed"
}

What this project shows

The refusal path is what I would want a reviewer to read. Most systems send a rejected file to the person who can do nothing about it; this one sends it to the seller with the reason, and the message was fixed after the first real one truncated.

Sharing a payment stack with the host shop was the right call, and it bit me once on every one of the six paths. Keeping the bridge while removing its side effects, instead of rewriting the gateways, was the honest trade.