Skip to main content
Restaurant Tech & POS

Restaurant Kiosk Food-Cost Engine

Recipe costing and live inventory, driven by every order placed at the kiosk

Project Overview

We added a food-cost and inventory module to a self-service ordering and kiosk platform for restaurants that serves a multi-language European market. The platform already took orders. What it lacked was any link between a sale and the ingredients that sale used up. We built a food-cost engine next to the existing ordering system. Purchases, recipes, waste and stock counts all post to a single stock-movement ledger, and each order and refund updates that ledger automatically through queued jobs. Merchants can now see what a dish actually costs them and where their stock goes.

Skills Applied

Restaurant Inventory SystemsRecipe CostingLaravel QueuesData MigrationMulti-Language DataDomain Modelling

The Challenge

Restaurants buy ingredients in one unit and use them in another: a case, a bag or a kilo comes in, and grams or single pieces go out. A recipe may call for a fraction of a unit, and those fractions add up quickly across a busy service. Refunds have to put stock back, spoilage has to be recorded with a reason, and purchase prices carry VAT that has to be split from the net cost. The menu data added another difficulty. It was multi-language, and the same dish or ingredient often existed as separate rows under different languages or spellings. That produced duplicate listings and put stock deductions at risk of landing on the wrong ingredient. Everything also had to go live on a production platform that was already taking orders, without disrupting service or corrupting existing records.

Our Solution

We built the engine as a merchant- and branch-scoped module in Laravel with an Inertia and React interface. Each ingredient has a purchase unit, a consumption unit and a conversion rate. A costing service works out cost per consumption unit and converts purchased quantities into consumption quantities. Recipe lines and stock quantities are stored as decimals to four places, so fractional portions stay exact. When an order is placed, a queued job multiplies the net quantity sold (quantity minus anything refunded) by each recipe line. It expands ingredient bundles into their components and writes consumption events and stock movements inside a database transaction. A matching job reverses consumption when an order is refunded. Utility-type ingredients record consumption without touching stock, so their balance never runs negative. Purchase lines take a merchant-defined VAT rate, and the system stores net and gross unit cost separately. For the multi-language data, ingredient sync matches records by source ID first and then by normalised name, slug and translated names, which stops the same ingredient being created twice. Menu variants of the same dish are grouped for display, but stock deductions only follow recipe links that a user has explicitly confirmed. Name matching can suggest a link but never decides one. For go-live we built a pre-deploy health check covering the queue driver, required tables, storage and failed jobs. We made the data backfill migrations idempotent and safe to re-run, and gave destructive clean-up commands a dry run by default, with a branch cross-check and an explicit apply flag.

Technical Stack

The technologies and tools we used to build this solution

Frontend

React 18Inertia.jsTailwind CSSVite

Backend

Laravel 10PHP 8Queued JobsArtisan Commands

APIs & Libraries

Eloquent ORMDomPDFCSV ExportRole-Based Middleware

Infrastructure

MariaDBQueue WorkersIdempotent MigrationsPre-Deploy Health Check

Key Features Implemented

Core functionality and technical implementations

Purchase-to-Consumption Unit Conversion

Each ingredient has its own purchase unit, consumption unit and conversion rate, so a case bought at one price becomes a correct cost per gram, millilitre or piece.

Fractional Recipe Deductions

Recipe lines are stored to four decimal places and multiplied by the net quantity sold. Bundles expand into their component ingredients, and each component moves in stock separately.

Stock Movement Ledger

Purchases, consumption, refunds, waste, opening balances, adjustments and stock counts are all signed movements in one ledger, each linked back to its source record.

Waste Logs With Reasons

Waste is logged against merchant-defined reasons and a responsible person, posts its own stock movement, and is valued at the unit cost that applied at the time.

VAT-Aware Purchasing

Purchase lines can carry a merchant VAT rate. The system stores net and gross unit cost separately, so cost can be read with or without VAT.

Cross-Language Ingredient Canonicalisation

Ingredients are matched by source ID and then by normalised name, slug and translations. Matched variants are collapsed in listings, and stock deductions stay tied to user-confirmed links.

Results & Impact

What this project achieved

  • Every kiosk order and refund now updates ingredient stock automatically through queued jobs, with no manual stock entry
  • Fractional recipe quantities are deducted exactly instead of being rounded to whole units
  • Merchants can see a cost per consumption unit for every ingredient, net of VAT
  • Language variants of the same dish appear once in costing lists, and an ingredient synced under a translated name is matched to the existing record instead of being duplicated
  • Stock is deducted only through recipe links a user has confirmed, never through a guessed name match
  • The move from development to live is gated by a health check, backfills that can be re-run without duplicating or altering existing records, and clean-up tools that report before they delete

Have a Similar Project in Mind?

We'd love to discuss how we can bring your vision to life with the same level of technical excellence.