End-to-end B2B wholesale platform with an admin web panel, a Flutter mobile ordering app for clients, and a Laravel API with FEFO inventory, a strict order state machine, and multi-tier pricing.
284K
Revenue
41
Orders
98%
Stock
ORD-0041
Atlas Import Co.
ORD-0040
Souss Export Ltd
ORD-0039
Maghreb Transit
Browse catalog
AR/FR · 3 price tiers
Cart & checkout
Real-time sync
Order history
Status tracking
FEFO allocation
Lot 001 → 002 → 003 (expiry order)
Overview
Grossiste Pro is a full-stack B2B wholesale platform built for distributors and their clients. It replaces manual phone/WhatsApp ordering with a structured digital workflow - clients browse a bilingual catalog on the mobile app, place orders with their negotiated pricing tier, and the warehouse team processes everything from the admin panel with full inventory visibility.
The backend is a Laravel 12 modular monolith with 8 domain modules, a strict FEFO lot allocation system, an enforced order state machine, and HMAC-signed requests for both the web panel and the mobile app.
Objective
Give wholesale clients a self-service mobile app to browse the bilingual catalog, manage their cart, and place orders without calling the distributor
Replace spreadsheet-based stock tracking with a FEFO lot inventory system that prevents overselling, tracks expiry, and records every movement
Enforce a strict order lifecycle - from placement through processing, delivery, and optional return - with every transition logged and guarded by business rules
Support multi-tier pricing (retail, wholesale, semi-wholesale) and a POS channel for in-person onsite sales alongside the app channel
My Role
Backend Architecture & API
Designed and built the Laravel 12 modular monolith - 8 domain modules each with its own models, service layer, repository pattern, DTOs, and API routes. HMAC authentication, Spatie RBAC, Laravel Horizon for queues.
Stateful Inventory System
Implemented FEFO lot allocation with row-level locking and DB transactions to prevent race conditions. Lot fields track initial, available, and reserved quantities separately - expired lots are never allocated.
Order State Machine
Built an enforced transition graph (pending → processing → delivering → delivered → returned, + onsite channel) via an OrderTransitionService - invalid transitions are rejected, every transition is logged with the acting user.
Admin Web Panel
React + TypeScript SPA with TanStack Router, Zustand stores, React Hook Form + Zod. Covers products, orders, POS, inventory, purchases, returns, clients, suppliers, roles, pricing rules, and financial accounts.
B2B Mobile App
Flutter app for wholesale clients with Riverpod state management, GoRouter, Dio with auto-refresh interceptor, and flutter_secure_storage. Features catalog browsing, cart, order history, and AR/FR localization.
Bilingual Catalog
Arabic and French fields throughout - product names, descriptions, and category names all bilingual. Missing French translations are auto-generated via the Google Translate API so admins can add Arabic-first.
Backend
Admin Panel
Mobile App
Core Features
Product: Huile Olive Extra — Lot allocations (FEFO)
LOT-2024-001
Expires 2025-03-15
200
Initial
0
Available
0
Reserved
LOT-2024-018
Expires 2025-09-20
500
Initial
140
Available
60
Reserved
LOT-2025-003
Expires 2026-06-10
300
Initial
300
Available
0
Reserved
FEFO rule: Lot-2024-018 is allocated first — its expiry date is earliest among non-exhausted lots. available_quantity is the single source of truth for allocation.
Inventory System
This is not a CRUD stock field - it's a stateful inventory system. Each product lot tracks three separate quantities: initial (immutable reference), available (can be allocated), and reserved (held by active orders). FEFO ensures the earliest-expiry lot is always consumed first.
Race condition prevention
All allocation runs inside DB transactions with SELECT FOR UPDATE. Two concurrent orders cannot over-allocate the same lot.
Partial lot allocation
A single order item can span multiple lots - FEFO fills from the earliest-expiry lot first and overflows into the next.
Lifecycle per channel
App orders reserve stock (available −qty, reserved +qty). Onsite/POS orders consume immediately (available −qty, OUT movement). Cancellations release reservations.
Full movement log
Every stock change - IN (purchase), OUT (delivery), RETURN - is recorded as an inventory movement for audit and reporting.
Order State Machine
Invalid transitions are rejected. Every valid transition appends a status log entry recording the acting user, the previous status, the new status, and a timestamp.
Place (app / wholesale)
Mobile client places order. Stock reserved via FEFO allocation.
Channel: app
Place (app / retail)
Retail client places order. Treated as processing immediately.
Channel: app · retail
Place (onsite / POS)
Admin creates onsite order. Stock consumed immediately, no reservation.
Channel: onsite
Process
Admin confirms the order. Guards: no expired lot reservations.
Channel: admin
Deliver
Releases reservations and records OUT movement in inventory.
Channel: admin
Return
Restores available_quantity and records RETURN movement on restockable items.
Channel: admin
Admin Panel
The React admin panel covers every module of the backend - from catalog and orders to inventory movements, purchase orders, returns, client CRM, and RBAC.
Screenshots
Admin Panel

Dashboard

Order Management

Order Details

Product Details

Inventory

Print Settings

Arabic Support
Mobile App

Product Catalog

Shopping Cart
Architecture
The backend is a Laravel 12 modular monolith - not a CRUD app. Every module (Product, Order, Inventory, Cart, Client, Supplier, Purchase, Media) has its own service layer, repository interface, DTOs, and policies. Controllers are thin: validation + response only. All domain logic lives in services.
Key Takeaways
Domain invariants are code, not documentation
FEFO, stock non-negativity, and state machine transitions are enforced in services - not in controllers or comments. If the rule isn't in code, it doesn't exist.
Concurrency requires explicit design
Every stock operation needs a DB transaction and row-level locking. A high-traffic B2B platform will hit concurrent allocation - it has to be correct by design, not by luck.
Mobile client changes the UX contract
A mobile ordering app for B2B clients shifts the product entirely - the catalog UX, cart sync, and order status need to work offline-tolerant and fast on mobile, not just on desktop.
Bilingual is a data model choice
AR/FR fields on every entity - not a translation table that joins later - keeps queries simple and cache-friendly. Auto-translation removes the friction of entering both languages manually.
Work with me
Whether it's a wholesale ordering system, an inventory-heavy backend, or a mobile app for your clients - I help you design the right data model before writing a line of code.