Skip to content

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_tips object: Each channel stores a collect_tips object with three keys — enabled (bool), options (array of percentages), and calculate_tips (enum). Default: { enabled: false, options: [10, 15, 20], calculate_tips: "After taxes" }.
  • Calculate Tips enum: calculate_tips is the CalculateTipsOptions enum with exactly two values, "After taxes" and "Before taxes" (capitalized, with a space — these are the stored/API values, not after_taxes/before_taxes).
  • Mutually exclusive tip input: A checkout request may send a tip_percentage OR a tip_amount, but not both. TransactionService::validateTransactionRequest() throws InvalidArgumentException('Cannot specify both tip percentage and tip amount') if both are present.
  • Percentage takes precedence: In calculateTips() (PriceTrait), if tip_percentage is set it is used; only otherwise is tip_amount used (confirmed by the calculate_tips_prefers_percentage_over_amount unit test).
  • Transaction storage: Tips are persisted on the Transaction as tip_amount (money, rounded to 2 dp) and tip_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 tipCalculatorActivated flag from the Kassanet API instead of the Upvendo collect_tips.enabled setting.

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/settingsCollectTips.vue (mode settings), embedded in InHouseSettingsForm.vue. Stores in_house_setting.collect_tips.
  • Online Settings: /online-settingsCollectTips.vue (mode online-settings), embedded in OnlineSettingsForm.vue. Stores online_settings.collect_tips.

Only these two surfaces have a tip setting. CollectTips.vue is rendered in exactly two places — InHouseSettingsForm.vue and OnlineSettingsForm.vue. Its qr-ordering mode branch is dead: no caller passes it, the QR Ordering page has no tips markup, and StoreQrOrderingRequest has no collect_tips rules. views/device-profiles/forms/Tipping.vue also exists but is dead codeDeviceProfileForm.vue does not import it and nothing in src/ 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 no collect_tips rules.
  • StoreOnlineSettingsRequest (collect_tips.* rules) — Online.
  • Constants::getDefaultInHouseSettings() / getDefaultOnlineSettings() — defaults.

Fields

Tips Enabled

PropertyValue
Field IDcollect_tips.enabled
LabelCollect Tips
TypeToggle (boolean)
Defaultfalse

Description: Enable tip collection for the channel. Validated required|boolean.


Tip Options

PropertyValue
Field IDcollect_tips.options
LabelTip Percentages
TypeArray 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

PropertyValue
Field IDcollect_tips.calculate_tips
LabelCalculate Tips
TypeSelect / Radio (CalculateTipsOptions enum)
OptionsBefore taxes, After taxes
DefaultAfter 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 - discount on standard channels, the raw subtotal on 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 total

Business Rules

  • Tips are applied only when the channel's collect_tips.enabled is true (for Kassanet locations, when the POS tipCalculatorActivated flag is "true" instead).
  • A request may contain tip_percentage OR tip_amount, never both; sending both throws InvalidArgumentException ("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 raw subtotal — the discount is not subtracted — because the tip is gated and calculated against the POS's own figures (TransactionService.php:694 vs :712).
  • Default collect_tips is { 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 requires min:1; both cap at max:100. Options are required when tips are enabled.
  • calculate_tips enum 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_amount instead of a tip_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 tipCalculatorActivated flag instead of collect_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) and tip_percentage (float), each rounded to 2 decimals, on the Transaction.

Troubleshooting

  • Tips not applied on kiosk/in-house → Verify collect_tips.enabled is true for that channel. For Kassanet (Hendrickx/Vanhoutte) locations, check the POS tipCalculatorActivated flag.
  • Validation error on checkout when adding a tip → A request must not include both tip_percentage and tip_amount; sending both raises InvalidArgumentException.
  • Tip percentage looks off vs. tax → The backend tip base is pre-tax: subtotal − discount on standard channels, but the raw subtotal (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 allow 0), 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_tips lives on In-House, Online, QR, and Device Profile settings.
  • Kassanet Integration: Hendrickx/Vanhoutte tip gating via tipCalculatorActivated.

Affects

  • Transactions: tip_amount and tip_percentage stored on the Transaction.
  • Payments / Totals: tip is added to the order total.

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"
  }
}