Skip to content
Ozan Doruk Yavuz logo Ozan Doruk Yavuz

Labonyx Dealer & Branch Ordering Portal

A full-stack B2B ordering portal I built for Labonyx, a scientific laboratory supplies company, so its dealer and branch network can place orders while the main company keeps the stock. It has role-based access with fifteen granular permissions, a branch hierarchy where sub-branches order against their parent's shared balance and inherited discount and never see prices, tiered discounts and payment terms, multi-currency pricing, online card payments through Garanti BBVA's 3D Secure virtual POS, and a white-label design panel. PostgreSQL is the source of truth; Google Sheets is kept only as a live monitoring mirror. Built with FastAPI and React, tested per endpoint, and shipped on Railway behind real security and rate limiting.

  • Python
  • FastAPI
  • React
  • PostgreSQL
  • Redis
  • Tailwind CSS

Summary. A full-stack B2B ordering portal I built for Labonyx, a scientific laboratory supplies company, so its dealer and branch network can order while the main company keeps the stock. It has role-based access with fifteen granular permissions, a branch hierarchy where sub-branches order against a shared balance and an inherited discount and never see prices, tiered discounts and payment terms, multi-currency pricing, a white-label design panel, and email confirmations. PostgreSQL is the source of truth and Google Sheets is kept only as a live monitoring mirror. It is built with FastAPI and React, tested per endpoint, and shipped on Railway.

The LABONYX Portal sign-in page with email and password fields and a language switcher
The portal sign-in. Branches and the main company sign in here to order and manage the catalogue, in Turkish or English.

Unlike the internal ERP I refactored, this one I designed and built from the start, so it is the cleaner story: typed models, one job per module, a test for every route, and a pipeline that runs them on every push. The interesting parts are the domain rules, the data architecture, and the fact that a non-technical owner can run and restyle the whole thing without me.

People and permissions

Access is role-based. There are five roles, from system administrator down to sub-branch, built on fifteen fine-grained permissions such as view the price list, create an order, cancel an order, manage balance, manage branches, and manage the design. Permissions are checked on every protected endpoint, not just hidden in the menu, and an administrator can compose new roles from the permission set. People sign in with email and password over short-lived tokens that carry their roles, and new accounts can self-register but stay pending until an admin approves them.

A hierarchy that shares money and hides prices

The core domain rule is the branch hierarchy. The main company holds all the stock and the money; each main branch carries a credit balance and a discount, and its sub-branches inherit both. When a sub-branch orders, the total comes off the parent main branch’s balance, the parent’s discount applies, and the sub-branch sees no prices at all. An order deducts the balance and a cancellation refunds it.

Main branch holds the credit balance sets the discount sees prices Sub-branch spends the parent's balance inherits the discount no prices shown Sub-branch spends the parent's balance inherits the discount no prices shown inherits inherits
Sub-branches inherit a parent branch’s balance and discount and never see prices. An order deducts the balance; a cancellation refunds it.

On top of that sit the commercial details: per-product VAT, tiered discounts, per-customer payment terms, special price lists, and multi-currency display in dollars, lira, and euro. At checkout, anyone allowed to see prices gets the full breakdown, from subtotal through discount and VAT to the grand total. Those who are not see nothing at all.

Taking card payments through the bank

Branches can pay by card, so I integrated Garanti BBVA’s Sanal POS, the bank’s virtual point of sale, with 3D Secure. The whole flow is built around a payment intent. When a branch checks out, the app creates an intent that snapshots the cart, reserves the stock, locks in the exchange rate, and holds for thirty minutes, then hands the customer to the bank’s own hosted page to enter the card and pass the 3D Secure step. The card details never touch the application, which keeps it out of the sensitive part of card handling. When the bank posts the result back, the first thing the app does is verify the signature on that callback, so a forged success can never confirm an order.

Checkout cart, delivery, and totals Create a payment intent reserve stock, lock the rate, hold for 30 minutes Redirect to Garanti’s hosted page 3D Secure; the card is entered on the bank Verify the bank’s signed callback reject anything not genuinely from the bank Approved order confirmed Declined intent released Abandoned swept, stock restored the bank posts back
Checkout reserves stock behind a 30-minute intent, the card is handled on the bank’s own 3D Secure page, and the signed callback decides the outcome. Abandoned sessions are swept and their stock is restored.

The hard parts were in the details the bank’s documentation does not spell out. The bank’s hashes have to be built over Turkish ISO-8859-9 text, because a Turkish character in a company name silently breaks them under the usual encoding. The bank also signs its responses two different ways depending on the path the payment took, so the verification has to accept either rather than reject a legitimate reply. And because a customer can simply close the tab on the bank’s page, the app would otherwise hold that reserved stock forever. A background sweeper finds intents that started but never finished, expires them with a compare-and-set that can never collide with a late callback, restores the stock, and records it in the audit log. Refunds go back through the bank the same way, each with its own signed request. Money is handled in minor units throughout, so the amounts are exact.

PostgreSQL as the source of truth, Sheets as a window

The owner wanted to keep watching the business in a Google Sheet, the way the company always had. Rather than build the app on a spreadsheet, I inverted it: PostgreSQL holds everything that matters and Sheets becomes a one-way monitoring mirror. The product catalogue is read from Sheets on a fifteen-minute cache, and orders and branches are written back to Sheets in the background, fire and forget, so a slow or failed write never touches the customer. A spreadsheet failure can never corrupt an order.

PostgreSQL source of truth: orders, branches, users, roles, balances, settings Google Sheet: Products the catalogue Google Sheet: monitoring orders and branches, to watch React app, FastAPI API every screen, every order read in, 15-min cache write out, background reads and writes
The catalogue flows in from a sheet on a cache; orders and branches flow back out to a sheet in the background. The app itself only ever touches PostgreSQL.

White-label by design

The portal is meant to be handed over and made the company’s own. A design panel lets an administrator restyle it from the browser: theme colours, the logo, banners, and the notification bar, all driven by CSS variables that come from the database, so changes take effect without a deploy. Uploaded images live in the database and are served through the API rather than sitting on disk. Every piece of text is translated, English and Turkish, on both the frontend and the backend, so even error messages come back in the right language. Admins can also post targeted notification banners to specific roles or people.

Built to be trusted, and to last

Security is not bolted on. Passwords are hashed with bcrypt and must meet a strength policy, login is timing-safe and rate-limited, tokens expire, uploads are validated by content type, and rich text is sanitised. Every response carries a strict set of security headers and a content security policy, and the sensitive actions, such as logins, orders, cancellations, and role and balance changes, are recorded in an audit log that system administrators can read. The schema only ever changes through migrations, every router has its own tests with external services mocked, and a pipeline runs the whole suite and builds the frontend on each push. In production a single service serves the API and the built React app, with Redis behind the catalogue cache and the rate limiter, and a safe fallback when it is absent.

Questions

Who uses this portal?

Labonyx’s dealers and branches, alongside the main company’s own staff. The main company holds the stock; branches and sub-branches place orders against it through the portal.

How do sub-branches work?

A sub-branch orders against its parent branch’s shared balance and inherited discount, and never sees prices anywhere in the interface. The parent branch sees both its own and its sub-branches’ order history.

Why keep Google Sheets if PostgreSQL is the database?

PostgreSQL is the real source of truth for orders, branches, users, balances, and settings. Sheets stays only as a live, read-only window so the owner can watch products, orders, and branches in a familiar spreadsheet, without it ever being load-bearing.

Is it multi-language and themeable?

Yes. Every string comes from English and Turkish files on both the frontend and the backend, and a design panel lets a non-technical admin restyle the whole portal, including colours, logo, and banners, from the browser, with the settings stored in the database.