Appearance
Tips Collection
Overview
Tips collection lets customers add a voluntary gratuity to their order. There is no dedicated "Tips" page or top-level route — tipping is a per-channel setting (collect_tips) configured inside each channel's settings page.
Key Purpose: Enable and configure tip collection per ordering channel (In-House, QR Ordering, Online Ordering, and Kiosk device profiles).
Purpose
This feature enables a tip option during checkout, with configurable percentage buttons and a calculation-base preference. It is configured independently for each channel; the back-office reuses one settings component (CollectTips.vue) across In-House, QR Ordering and Online Ordering, and a separate Tipping form for Kiosk device profiles.
Key Concepts
- No standalone tips page: Tipping is a settings feature, not a route. It lives under each channel's settings: In-House Settings and Online Settings.
collect_tipsobject: Each channel stores acollect_tipsobject with three keys —enabled(bool),options(array of percentages), andcalculate_tips(enum). Default:{ enabled: false, options: [10, 15, 20], calculate_tips: "After taxes" }.- Calculate Tips enum:
calculate_tipsis theCalculateTipsOptionsenum with exactly two values,"After taxes"and"Before taxes"(capitalized, with a space — these are the stored/API values, notafter_taxes/before_taxes). - Mutually exclusive tip input: A checkout request may send a
tip_percentageOR atip_amount, but not both.TransactionService::validateTransactionRequest()throwsInvalidArgumentException('Cannot specify both tip percentage and tip amount')if both are present. - Percentage takes precedence: In
calculateTips()(PriceTrait), iftip_percentageis set it is used; only otherwise istip_amountused (confirmed by thecalculate_tips_prefers_percentage_over_amountunit test). - Transaction storage: Tips are persisted on the Transaction as
tip_amount(money, rounded to 2 dp) andtip_percentage(float, rounded to 2 dp). - Kassanet POS override: For Kassanet-integrated menus/locations (Hendrickx / Vanhoutte), tip availability is gated by the POS system's
tipCalculatorActivatedflag from the Kassanet API instead of the Upvendocollect_tips.enabledsetting.
Actions
Enable Tip Collection
Toggle collect_tips.enabled in the relevant channel's settings: In-House Settings or Online Settings.
Configure Percentage Options
Set three percentage values (default: 10, 15, 20). The back-office UI provides exactly three percentage fields. Validation: In-House options must be numeric min:0, max:100; Online options must be numeric min:1, max:100. Note the backend allows 0 for In-House, but the back-office form validates all three percentage fields as >= 1, so in practice every configured option is at least 1%. The options array is required (required) when collect_tips.enabled is true, otherwise nullable.
Set Calculation Base
Choose "After taxes" (default) or "Before taxes". This value is stored and exposed to the client; note the important caveat in Business Rules below about how the backend actually computes the tip.
Customer: Select Tip
On the customer-facing checkout, the shopper picks a percentage button or enters an amount. (The exact customer-facing tip-prompt UI — kiosk keypad, online inline input, "No Tip" option — is rendered by the storefront/kiosk clients and is not verified in this backend/back-office review.)
Location
Tipping is configured in these back-office places (no /tips route exists):
- In-House Settings:
/in-house/settings—CollectTips.vue(modesettings), embedded inInHouseSettingsForm.vue. Storesin_house_setting.collect_tips. - Online Settings:
/online-settings—CollectTips.vue(modeonline-settings), embedded inOnlineSettingsForm.vue. Storesonline_settings.collect_tips.
Only these two surfaces have a tip setting.
CollectTips.vueis rendered in exactly two places —InHouseSettingsForm.vueandOnlineSettingsForm.vue. Itsqr-orderingmode branch is dead: no caller passes it, the QR Ordering page has no tips markup, andStoreQrOrderingRequesthas nocollect_tipsrules.views/device-profiles/forms/Tipping.vuealso exists but is dead code —DeviceProfileForm.vuedoes not import it and nothing insrc/references it. Kiosk tipping is inherited from the location's In-House settings, not configured per device profile.
Backend persistence/validation:
StoreInHouseSettingsRequest(collect_tips.*rules) — In-House only. QR ordering has nocollect_tipsrules.StoreOnlineSettingsRequest(collect_tips.*rules) — Online.Constants::getDefaultInHouseSettings()/getDefaultOnlineSettings()— defaults.
Fields
Tips Enabled
| Property | Value |
|---|---|
| Field ID | collect_tips.enabled |
| Label | Collect Tips |
| Type | Toggle (boolean) |
| Default | false |
Description: Enable tip collection for the channel. Validated required|boolean.
Tip Options
| Property | Value |
|---|---|
| Field ID | collect_tips.options |
| Label | Tip Percentages |
| Type | Array of Numbers (three values in the UI) |
| Default | [10, 15, 20] |
Description: Percentage options. In-House: each numeric, min:0, max:100 (but the form enforces >= 1). Online: each numeric, min:1, max:100. Required when enabled is true.
Examples:
[10, 15, 20]— Standard (default)[15, 18, 20]— Higher options[5, 10, 15]— Lower options
Calculate Tips
| Property | Value |
|---|---|
| Field ID | collect_tips.calculate_tips |
| Label | Calculate Tips |
| Type | Select / Radio (CalculateTipsOptions enum) |
| Options | Before taxes, After taxes |
| Default | After taxes |
Description: Stored preference for the tip-percentage base. Persisted and returned exactly as "After taxes" / "Before taxes".
Caveat: see Business Rules — the backend tip computation in
calculateTipsForTransaction()/calculateTips()uses a pre-tax base —subtotal - discounton standard channels, the rawsubtotalon Kassanet menus — and does not branch on this enum. The value is a stored/display preference consumed by clients.
Per-Channel Settings
In-House (and QR Ordering)
json
{
"in_house_setting": {
"collect_tips": {
"enabled": true,
"options": [10, 15, 20],
"calculate_tips": "After taxes"
}
}
}Online Ordering
json
{
"online_settings": {
"collect_tips": {
"enabled": true,
"options": [10, 15, 20],
"calculate_tips": "After taxes"
}
}
}Kiosk
Kiosk tipping is not configured on the device profile. The kiosk inherits the location's In-House settings (/in-house/settings).
Business Logic
Tip Calculation (as implemented in backend)
The backend computes the tip from the request input against a pre-tax base: subtotal − discount on standard channels, or the raw subtotal on Kassanet-integrated menus:
Subtotal: €25.00
Discount: €0.00
Tip base used: €25.00 (subtotal − discount, pre-tax)
If request sends tip_percentage = 15:
tip_amount = 15% of €25.00 = €3.75
If request sends tip_amount = €4.00 (no percentage):
tip_amount = €4.00
tip_percentage = (4.00 / 25.00) * 100 = 16.00 (derived, rounded to 2 dp)tip_amount and tip_percentage are then stored on the Transaction (each rounded to 2 decimals).
Tip Flow
Customer at checkout
│
▼
Tips enabled? (collect_tips.enabled, or Kassanet tipCalculatorActivated)
├── No → no tip applied
│
└── Yes → client shows tip options
├── Percentage selection → request carries tip_percentage
└── Amount entry → request carries tip_amount
│
▼
Backend: validateTransactionRequest()
(rejects if BOTH tip_percentage AND tip_amount present)
│
▼
calculateTips() on (subtotal − discount), percentage wins over amount
│
▼
Store tip_amount + tip_percentage on Transaction; add to totalBusiness Rules
- Tips are applied only when the channel's
collect_tips.enabledis true (for Kassanet locations, when the POStipCalculatorActivatedflag is"true"instead). - A request may contain
tip_percentageORtip_amount, never both; sending both throwsInvalidArgumentException("Cannot specify both tip percentage and tip amount"). - When both could be inferred, the percentage path takes precedence over the amount path in
calculateTips(). - The tip base used by the backend is pre-tax and does not depend on the
calculate_tips("After taxes" / "Before taxes") setting; that enum is a stored/client preference, not a backend calculation switch. - The base differs by channel. Standard (non-Kassanet) channels compute the tip on
subtotal − discount. For Kassanet-integrated menus (Hendrickx / Vanhoutte) the base is the rawsubtotal— the discount is not subtracted — because the tip is gated and calculated against the POS's own figures (TransactionService.php:694vs:712). - Default
collect_tipsis{ enabled: false, options: [10, 15, 20], calculate_tips: "After taxes" }. - Percentage options: the In-House backend rule allows
min:0(though the form enforces>= 1); Online requiresmin:1; both cap atmax:100. Options are required when tips are enabled. calculate_tipsenum values are stored/validated as the strings"After taxes"and"Before taxes"(not lowercase/underscore).
FAQs
- "Where do I configure tip settings?" There is no separate Tips page. Configure tips per channel: In-House Settings (
/in-house/settings) or Online Settings (/online-settings). Those are the only two surfaces with a tip setting — QR ordering and kiosk device profiles have none, and the kiosk inherits the location's In-House settings. - "What is the difference between 'Before Taxes' and 'After Taxes'?" It is a stored preference describing the intended tip-percentage base. Note that the backend tip computation always uses the pre-tax subtotal minus discount; the enum is surfaced to clients rather than branching the server-side calculation.
- "Can a customer enter a custom amount?" The checkout can submit a
tip_amountinstead of atip_percentage(the two are mutually exclusive). The exact customer-facing custom-amount / "No Tip" UI is rendered by the storefront/kiosk clients and is not verified in this backend/back-office review. - "Do tips work with Kassanet POS integrations?" Yes, but for Hendrickx/Vanhoutte the system uses the Kassanet API's
tipCalculatorActivatedflag instead ofcollect_tips.enabled. If the POS reports tips off, no tip is applied regardless of Upvendo settings. - "How is the tip stored?" As
tip_amount(money) andtip_percentage(float), each rounded to 2 decimals, on the Transaction.
Troubleshooting
- Tips not applied on kiosk/in-house → Verify
collect_tips.enabledis true for that channel. For Kassanet (Hendrickx/Vanhoutte) locations, check the POStipCalculatorActivatedflag. - Validation error on checkout when adding a tip → A request must not include both
tip_percentageandtip_amount; sending both raisesInvalidArgumentException. - Tip percentage looks off vs. tax → The backend tip base is pre-tax:
subtotal − discounton standard channels, but the rawsubtotal(discount not subtracted) on Kassanet-integrated menus (Hendrickx / Vanhoutte). The "After/Before taxes" setting does not change this server-side calculation either way. - Can't save tip options → When tips are enabled, the options array is required; Online options must be
≥ 1(In-House/QR allow0), all≤ 100.
Customer Impact
The following describes intended customer behavior. The concrete tip-prompt UI is implemented in the storefront/kiosk clients and is not verified in this backend/back-office review.
- Customers can add a tip at checkout when the channel has tips enabled.
- Percentage buttons reflect the configured
options; an amount can be entered instead of a percentage. - The chosen tip is added to the order total and recorded on the transaction.
Relations
Depends On
- Locations / Settings:
collect_tipslives on In-House, Online, QR, and Device Profile settings. - Kassanet Integration: Hendrickx/Vanhoutte tip gating via
tipCalculatorActivated.
Affects
- Transactions:
tip_amountandtip_percentagestored on the Transaction. - Payments / Totals: tip is added to the order total.
Related Features
Examples
Standard Tip Setup
json
{
"collect_tips": {
"enabled": true,
"options": [10, 15, 20],
"calculate_tips": "After taxes"
}
}Higher Tip Options
json
{
"collect_tips": {
"enabled": true,
"options": [15, 18, 20],
"calculate_tips": "After taxes"
}
}Tips Disabled
json
{
"collect_tips": {
"enabled": false,
"options": [10, 15, 20],
"calculate_tips": "After taxes"
}
}