Skip to main content
LOYALTI — cover

Featured work

LOYALTI

Multi-tenant loyalty & bonus platform with a PremiumBonus-compatible POS API, typed wallets and a targeted-bonus allocator.

  • PHP 8.4
  • Symfony 8
  • PostgreSQL 18
  • Doctrine
  • Bitrix D7
  • Docker Swarm
  • GitLab CI
  • PHPStan L9

The problem

Loyalty programs are easy to demo and hard to get right. The money is in the redemption path: partial write-offs, targeted gifts that may only apply to some items, reservation holds that must not double-spend, and a POS that retries the same request three times over a flaky connection. Get any of it wrong and you either give money away twice or refuse a legitimate discount at the till.

Architecture

LOYALTI is a hexagonal Symfony 8 / PostgreSQL 18 backend. Every member has a wallet with typed balancesgifted, promo, earned — because they expire and redeem under different rules. Redemption runs through a two-phase allocator:

  • Phase 1 — targeted: grants that restrict to specific items or carts are consumed first, honouring per-item and per-cart caps, FIFO by nearest expiry, with reservation locks so leftover targeted grants can’t be burned by another flow.
  • Phase 2 — baseline: the remaining amount draws down general balances under the tenant’s max-redeem cap, with targeted spend exempt above that cap.

The POS surface implements the PremiumBonus Purchase200 specpurchase, reserve, write-off, reversal — all idempotent by external purchase id, so a retried request returns the original result instead of charging twice.

Highlights

  • Reservation flow with TTL holds and atomic SQL UPDATE decrements — no lost updates under concurrent tills.
  • A Bitrix D7 module integrates the platform into existing e-commerce: catalog sections, payment/delivery sync, and a 4-step redeem widget.
  • Full GitLab CI/CD to Docker Swarm: lint → PHPUnit (with throwaway Postgres/Redis/RabbitMQ) → build → docker stack deploy.

Results

721 PHPUnit tests / 1798 assertions, PHPStan level 9 clean, plus a black-box flow suite that drives the real POS endpoints. Multi-tenant from day one — one deployment, many merchants.

Lessons

  • Money code wants idempotency keys before it wants features. The purchase id contract removed a whole class of double-spend bugs.
  • Typed balances beat a single integer. The rules live in the type, not in scattered ifs.