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 balances — gifted, 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 spec — purchase, 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 UPDATEdecrements — 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.