Skip to content

Tax Rates

Overview

Tax Rates define the VAT/sales-tax categories applied to menu items and modifier groups. Each tax rate is a category (e.g. "Food", "Alcohol") that carries three separate percentages — one for each service type: dine-in, take-out, and delivery.

Key Purpose: Configure per-category, per-service-type tax rates so menu items are priced and reported correctly.

The Tax Rates page lives under the Menus navigation group and is only shown when a tax-rates-supported POS is connected. As of this writing the supported POS list (TAX_RATES_SUPPORTED_POS) is mpluskassa, shopcaisse, and lightspeed. If none of those is connected, the menu item is not added to the sidebar.

Purpose

This page lets you create, edit, view, and delete custom tax-rate categories for the currently selected location. Each rate stores three percentages (dine-in / take-out / delivery), a tax method (Inclusive or Exclusive), a description, and an auto-generated category code that menu items reference.

Key Concepts

  • Tax Rate Category: A named group (e.g. "Food", "Alcohol") rather than a single percentage. Items reference a category by its generated code.
  • Three Per-Service-Type Rates: Every tax rate stores dine_in_rate, take_out_rate, and delivery_rate independently, so the same category can be taxed differently depending on how the order is fulfilled.

Known limitation — online eat-in tax display. An online eat-in (For Here) order is charged the item's dine-in rate, but the Online Ordering storefront's displayed tax breakdown currently shows the take-out rate: the online-ordering menu payload only carries Delivery and Pickup rate keys, and the storefront maps For Here to Pickup. A visible mismatch only appears when the location's dine-in and take-out rates differ and the tax method is Exclusive. The Kiosk had the same issue and was fixed on 2026-06-24. The recorded and charged amount is authoritative, not the displayed one.

  • Tax Method: Inclusive (tax is already included in the product price) or Exclusive (tax is added on top). Defined by the TaxMethods enum.
  • Category Code: An uppercase code auto-derived from the category name. The backend computes it as strtoupper(preg_replace('/[^A-Za-z]/', '_', category)) — every non-letter character (digits and spaces) becomes an underscore. For example "Food 9" becomes FOOD__ (the space and the digit each become an underscore) and "Reduced VAT" becomes REDUCED_VAT. It must be unique per location/country across both custom and standard rates. The code is derived only when the rate is created; renaming the category later does not regenerate it.
  • Custom vs. Standard Rates: Custom rates are merchant-created (source Custom). Standard rates are system-provided country defaults (source EU VAT). Both live in the same tax_rates Mongo collection. Only rates with source: Custom are editable or deletable in the back office — standard (EU VAT) and POS-synced rates are read-only, and the list offers no Edit action for them.
  • Location-Country Binding: Tax rates are scoped to a location. The backend resolves the country code from the location via LocationService::retrieve()->getCountryCode() to check uniqueness against country-level standard rates.
  • POS-Synced Rates: Rates derived from a read-only inbound POS (ShopCaisse, Hendrickx, Vanhoutte) mirror that POS's VAT codes and are recreated on each sync. They are flagged via isPosSynced() and cannot be edited or deleted: the list shows no Edit action for them, and the API returns HTTP 422 if called directly.
  • In-Use Protection: A tax rate that is currently referenced by any item or modifier group (by tax_rate_code) cannot be deleted; the backend returns HTTP 409.

Actions

Create a Tax Rate

Open the Add dialog, enter a category name, choose a tax method, add a description, and set the dine-in / take-out / delivery percentages. New rates are always created with source Custom and scoped to the currently selected location. The backend generates the category code and rejects the request (409) if that code already exists for the location/country.

Edit a Tax Rate

Update the category, tax method, description, or any of the three rates for an existing custom rate. Only rates with source Custom carry an Edit action in the list; standard (EU VAT) and POS-synced rates have none, and the API returns 422 for a POS-synced rate if called directly.

Renaming the Category on an existing rate does not regenerate category_code — the code is derived only when the rate is first created. A rate renamed from "Food" to "Food Reduced" keeps the code FOOD, so items and modifier groups continue resolving through the original code. The duplicate-code 409 is likewise only raised on create; updates are never re-checked for code collisions.

Delete a Tax Rate

Remove a custom rate that is no longer needed. Deletion is blocked (409) if any item or modifier group still references the rate's category code, and is also blocked (422) for POS-synced rates.

Location

  • Backoffice Route: /menus/tax-rates (named route menus-tax-rates, under the Menus nav group)
  • Page Component: src/pages/menus/tax-rates/index.vue
  • View Component: src/views/tax-rates/TaxRates.vue
  • Dialog Component: src/components/modules/tax-rates/TaxRateDialog.vue
  • CRUD Controller: app/Http/Controllers/Api/BackOffice/TaxRateController.php (datatable / store / update / delete)
  • Read-only Options Controller: app/Http/Controllers/Api/TaxRateController.php (only an options() endpoint used to populate item tax-rate dropdowns)

Fields

Category

PropertyValue
Field IDcategory
LabelCategory Name (the list column header is "Category")
TypeText
RequiredYes
Validation`required

Description: The category name. The backend derives the unique category_code from this value.

Examples:

  • "Food"
  • "Alcohol"
  • "Standard"
  • "Reduced"

Tax Method

PropertyValue
Field IDtax_method
LabelTax Calculation Method
TypeSelect
RequiredYes
Validationrequired, must be one of the TaxMethods enum values

Allowed values (App\Enums\TaxMethods):

  • Inclusive — tax is included in the product price
  • Exclusive — tax is added on top of the product price

Description

PropertyValue
Field IDdescription
LabelDescription
TypeTextarea
RequiredYes
Validation`required

Description: A free-text description of the tax rate.


Dine-in Rate

PropertyValue
Field IDdine_in_rate
LabelDine-In Rate (%)
TypeNumber (percentage)
RequiredYes
Validation`required

Description: Tax percentage applied to dine-in orders.


Take-out Rate

PropertyValue
Field IDtake_out_rate
LabelTakeout Rate (%)
TypeNumber (percentage)
RequiredYes
Validation`required

Description: Tax percentage applied to take-out orders.


Delivery Rate

PropertyValue
Field IDdelivery_rate
LabelDelivery Rate (%)
TypeNumber (percentage)
RequiredYes
Validation`required

Description: Tax percentage applied to delivery orders.


Location

PropertyValue
Field IDlocation_id
Label(set automatically)
TypeString (location ID)
RequiredYes on create (`required

Description: The location the rate belongs to. The front end sets this from the globally selected location; it is not editable in the dialog. On update the location_id is preserved and not re-validated.


Derived / system fields

These are not entered by the user but are set by the backend:

FieldNotes
category_codeUppercase code derived from category (non-letters → _); unique per location/country. Set on create only — never recomputed on update.
country_codeResolved from the location via LocationService.
sourceSet to Custom for back-office-created rates. Standard rates use EU VAT; POS-synced rates carry the originating channel slug.

Common Tax Rates by Country

The actual standard (country-level) rates are stored as data in the tax_rates Mongo collection — there is no seeder for them in the repository, so the specific percentages cannot be verified from code. The table below is illustrative only and should not be treated as authoritative. Custom rates are entered per category and service type and are not derived from any built-in country table.

CountryTypical categories (examples, not code-verified)
Netherlands (NL)Standard and reduced VAT bands
Belgium (BE)Standard, reduced, and intermediate bands
Germany (DE)Standard and reduced bands

Always confirm the correct rates with local tax authorities or a tax professional.


Business Logic

Tax Method (Inclusive vs Exclusive)

Tax Method = Exclusive (tax added on top)
Item Price (excl. tax): €10.00
Dine-in Rate: 9%
Tax Amount = €10.00 × 0.09 = €0.90
Customer Pays = €10.90

Tax Method = Inclusive (tax already in price)
Item Price (incl. tax): €10.90
Dine-in Rate: 9%
Tax Amount = €10.90 − (€10.90 / 1.09) = €0.90
Net Price = €10.00

Per-Service-Type Rates

A single tax rate carries three independent percentages. The rate that applies to an order depends on the service type:

Tax Rate "Food":
├── dine_in_rate:   9%
├── take_out_rate:  9%
└── delivery_rate:  9%

Tax Rate "Alcohol":
├── dine_in_rate:   21%
├── take_out_rate:  21%
└── delivery_rate:  21%

(Percentages shown are illustrative.)

How items reference a tax rate

Items and modifier groups store a tax_rate_code (the uppercase category code), not a tax-rate ID. This is also what the in-use check inspects when deciding whether a rate can be deleted.


Customer Impact

Tax rates do not appear directly to customers, but they determine:

  • Whether displayed prices include or exclude tax (driven by the tax method).
  • The tax amount calculated per item and per order, based on the service type (dine-in / take-out / delivery).
  • The tax breakdown shown on receipts.

Relations

Depends On

  • Locations: Each tax rate is scoped to a location, and the country code is resolved from it.
  • POS connection: The page only appears when a tax-rates-supported POS (MplusKassa, ShopCaisse, or Lightspeed) is connected.

Affects

  • Menu Items: Each item references a tax rate by tax_rate_code.
  • Modifier Groups: Also reference a tax rate by tax_rate_code.
  • Transactions / Receipts: Tax is calculated using the per-service-type rate.

Business Rules

  • The category_code is generated as strtoupper(preg_replace('/[^A-Za-z]/', '_', category)) — digits and spaces both become underscores, so "Food 9" becomes FOOD__ and "Reduced VAT" becomes REDUCED_VAT — and must be unique per location/country across both the custom and standard tax-rate repositories.
  • The code is computed only on create. Renaming an existing rate's Category leaves category_code unchanged, so every item and modifier group keeps resolving through the original code.
  • Creating a rate whose category code already exists returns HTTP 409 with the message "Please use different category name." The update path performs no duplicate-code check, so this error can only ever be raised on create.
  • A tax rate cannot be deleted while it is referenced by any item or modifier group (isBeingUsed() checks both repositories by location_id + tax_rate_code); attempting to do so returns HTTP 409 with "Cannot delete tax rate that is being used."
  • Only rates with source Custom are editable or deletable in the back office. Standard (EU VAT) and POS-synced rates carry no Edit action in the list.
  • POS-synced rates (source = an in-house channel: ShopCaisse, Hendrickx, Vanhoutte) cannot be edited or deleted; if the API is called directly, both operations return HTTP 422 directing the change to the POS system.
  • The country code is resolved from the location's address via LocationService::retrieve()->getCountryCode().
  • All back-office-created rates are stored with source Custom.
  • The page and its actions are permission-gated: view-tax-rates to see the list, view-custom-tax-rates to see custom rate detail, and create-custom-tax-rates / edit-custom-tax-rates / delete-custom-tax-rates for the corresponding actions.

FAQs

  • "Why can I not delete a tax rate?" It is still referenced by one or more menu items or modifier groups. Reassign them to a different rate first.
  • "Why can I not edit this tax rate?" Only rates with source Custom can be edited here — standard (EU VAT) rates and rates synced from your connected POS show no Edit action. Change POS-synced rates in the POS system instead.
  • "Why does my new category get rejected as a duplicate?" Its generated category code collides with an existing custom or standard rate for the same location/country. Use a different category name. Note that only letters survive in the code, so names differing only in digits or punctuation collide: "Food 1" and "Food 2" both become FOOD__.
  • "I renamed a tax category — did the code change?" No. category_code is generated only when the rate is created, so a rate renamed from "Food" to "Food Reduced" keeps the code FOOD and every item pointing at it keeps working unchanged.
  • "Why don't I see the Tax Rates page?" It only appears when a tax-rates-supported POS (MplusKassa, ShopCaisse, or Lightspeed) is connected, and you need the view-tax-rates permission.
  • "Why are there three rate fields?" Each tax rate has separate percentages for dine-in, take-out, and delivery service types.

Troubleshooting

Problem: Tax Rates page is missing from the sidebar

Causes:

  1. No tax-rates-supported POS (MplusKassa, ShopCaisse, Lightspeed) is connected.
  2. The user lacks the view-tax-rates permission.

Solutions:

  1. Connect a supported POS.
  2. Grant the appropriate tax-rate permissions.

Problem: Cannot save or delete a tax rate

Causes:

  1. The rate is POS-synced (HTTP 422).
  2. The rate is in use by items or modifier groups (delete, HTTP 409).
  3. The category code duplicates an existing rate (create, HTTP 409).

Solutions:

  1. Make the change in the connected POS system.
  2. Reassign dependent items/modifier groups before deleting.
  3. Choose a different category name.

Problem: Wrong tax applied to an order

Causes:

  1. Item references the wrong tax_rate_code.
  2. The wrong service-type rate applies (dine-in vs take-out vs delivery).
  3. Tax method (Inclusive/Exclusive) set incorrectly.

Solutions:

  1. Check the item's assigned tax rate.
  2. Verify the dine-in / take-out / delivery percentages on the rate.
  3. Confirm the tax method matches how prices are entered.

Examples

Creating a custom tax rate (request payload)

json
{
  "category": "Food",
  "tax_method": "Inclusive",
  "description": "Reduced VAT band for food items",
  "dine_in_rate": 9,
  "take_out_rate": 9,
  "delivery_rate": 9,
  "location_id": "665f0a1b2c3d4e5f60718293"
}

The backend then derives category_code: "FOOD", resolves country_code from the location, and stores source: "Custom".

How an item references a tax rate

json
{
  "name": "Margherita Pizza",
  "price": 12.50,
  "tax_rate_code": "FOOD"
}

Items store the uppercase tax_rate_code, not a tax-rate ID.


  • VAT/sales-tax registration, display, and reporting obligations vary by country and business.
  • Receipts must accurately reflect the tax charged.
  • Standard country rates change over time; verify current rates with local authorities.

Note: Tax regulations vary by country. Consult a tax professional for compliance advice.