Appearance
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, anddelivery_rateindependently, 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) orExclusive(tax is added on top). Defined by theTaxMethodsenum. - 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" becomesFOOD__(the space and the digit each become an underscore) and "Reduced VAT" becomesREDUCED_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 (sourceEU VAT). Both live in the sametax_ratesMongo collection. Only rates withsource: Customare 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 routemenus-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 anoptions()endpoint used to populate item tax-rate dropdowns)
Fields
Category
| Property | Value |
|---|---|
| Field ID | category |
| Label | Category Name (the list column header is "Category") |
| Type | Text |
| Required | Yes |
| Validation | `required |
Description: The category name. The backend derives the unique category_code from this value.
Examples:
- "Food"
- "Alcohol"
- "Standard"
- "Reduced"
Tax Method
| Property | Value |
|---|---|
| Field ID | tax_method |
| Label | Tax Calculation Method |
| Type | Select |
| Required | Yes |
| Validation | required, must be one of the TaxMethods enum values |
Allowed values (App\Enums\TaxMethods):
Inclusive— tax is included in the product priceExclusive— tax is added on top of the product price
Description
| Property | Value |
|---|---|
| Field ID | description |
| Label | Description |
| Type | Textarea |
| Required | Yes |
| Validation | `required |
Description: A free-text description of the tax rate.
Dine-in Rate
| Property | Value |
|---|---|
| Field ID | dine_in_rate |
| Label | Dine-In Rate (%) |
| Type | Number (percentage) |
| Required | Yes |
| Validation | `required |
Description: Tax percentage applied to dine-in orders.
Take-out Rate
| Property | Value |
|---|---|
| Field ID | take_out_rate |
| Label | Takeout Rate (%) |
| Type | Number (percentage) |
| Required | Yes |
| Validation | `required |
Description: Tax percentage applied to take-out orders.
Delivery Rate
| Property | Value |
|---|---|
| Field ID | delivery_rate |
| Label | Delivery Rate (%) |
| Type | Number (percentage) |
| Required | Yes |
| Validation | `required |
Description: Tax percentage applied to delivery orders.
Location
| Property | Value |
|---|---|
| Field ID | location_id |
| Label | (set automatically) |
| Type | String (location ID) |
| Required | Yes 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:
| Field | Notes |
|---|---|
category_code | Uppercase code derived from category (non-letters → _); unique per location/country. Set on create only — never recomputed on update. |
country_code | Resolved from the location via LocationService. |
source | Set 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_ratesMongo 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.
| Country | Typical 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.00Per-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.
Related Features
Business Rules
- The
category_codeis generated asstrtoupper(preg_replace('/[^A-Za-z]/', '_', category))— digits and spaces both become underscores, so "Food 9" becomesFOOD__and "Reduced VAT" becomesREDUCED_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_codeunchanged, 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 bylocation_id+tax_rate_code); attempting to do so returns HTTP 409 with "Cannot delete tax rate that is being used." - Only rates with source
Customare 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-ratesto see the list,view-custom-tax-ratesto see custom rate detail, andcreate-custom-tax-rates/edit-custom-tax-rates/delete-custom-tax-ratesfor 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
Customcan 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_codeis generated only when the rate is created, so a rate renamed from "Food" to "Food Reduced" keeps the codeFOODand 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-ratespermission. - "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:
- No tax-rates-supported POS (MplusKassa, ShopCaisse, Lightspeed) is connected.
- The user lacks the
view-tax-ratespermission.
Solutions:
- Connect a supported POS.
- Grant the appropriate tax-rate permissions.
Problem: Cannot save or delete a tax rate
Causes:
- The rate is POS-synced (HTTP 422).
- The rate is in use by items or modifier groups (delete, HTTP 409).
- The category code duplicates an existing rate (create, HTTP 409).
Solutions:
- Make the change in the connected POS system.
- Reassign dependent items/modifier groups before deleting.
- Choose a different category name.
Problem: Wrong tax applied to an order
Causes:
- Item references the wrong
tax_rate_code. - The wrong service-type rate applies (dine-in vs take-out vs delivery).
- Tax method (Inclusive/Exclusive) set incorrectly.
Solutions:
- Check the item's assigned tax rate.
- Verify the dine-in / take-out / delivery percentages on the rate.
- 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.
Legal Compliance
- 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.