Include configuration for tax on tips (#7298)
What changed, and why it matters
This commit adds a new optional 'tax on tips' feature to BTCPay Server's Point of Sale app. Merchants can now configure a separate tax rate that applies to customer tips, and the receipt/cart display breaks out the tip tax separately from regular item tax. There is no indication this is a security fix; it is a normal business-logic feature addition.
No security action required; review as normal feature code. If auditing, verify that the client-side and server-side totals cannot be desynchronized and that tip tax is consistently rounded.
Security signals we found
New user-supplied numeric input (TipTaxRate) is validated with Range(0.0, 100.0) and server-side null/negative checks
No injection, authorization, or cryptographic changes observed
No vendor disclosure of security relevance
Evidence from the diff
The change introduces a TipTaxRate setting across the Point of Sale settings, view models, controller, order calculation logic, receipt data, and both server-side and client-side rendering. It stores taxOnTip as additional invoice metadata and adjusts totals so tip + tip tax are included correctly. Validation constrains the rate to 0-100 and rejects negative values. Tests cover cart and keypad scenarios.
Changed components
BTCPayServer.Plugins.PointOfSaleBTCPayServer.Services.Apps.PointOfSaleSettingsBTCPayServer.Services.Invoices.InvoiceEntity / PosReceiptData / WellKnownPosDataPoint of Sale public cart/keypad UIInvoice receipt viewsInspect captured patch +181 / −31
diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs
index 9a7cd2e..dfcd85e 100644
--- a/BTCPayServer.Tests/POSTests.cs
+++ b/BTCPayServer.Tests/POSTests.cs
@@ -619,6 +619,44 @@ goodies:
await s.TakeScreenshot("BadInventory.png");
throw;
}
+ await s.GoToUrl(editUrl);
+ await s.Page.FillAsync("#TipTaxRate", "5");
+ await s.ClickPagePrimary();
+ await s.FindAlertMessage(partialText: "App updated");
+
+ await s.GoToUrl(posUrl);
+ await s.Page.WaitForSelectorAsync("#PosItems");
+ await s.Page.ClickAsync(".posItem:nth-child(1) .btn-primary");
+ await s.Page.ClickAsync("#Tip-10");
+
+ await AssertCartSummary(s, new()
+ {
+ Subtotal = "1,00 €",
+ Taxes = "0,10 € (10%)",
+ Tip = "0,10 € (10%)",
+ TaxOnTip = "0,01 €",
+ Total = "1,21 €"
+ });
+
+ await s.Page.ClickAsync("#CartSubmit");
+ await s.Page.WaitForSelectorAsync("#Checkout");
+ await s.PayInvoice(true);
+
+ await s.Page.ClickAsync("#ReceiptLink");
+ await s.Page.WaitForSelectorAsync("#CartData table");
+ await AssertReceipt(s, new()
+ {
+ Items = [
+ new("Green Tea", "1 x 1,00 € = 1,00 €")
+ ],
+ Sums = [
+ new("Subtotal", "1,00 €"),
+ new("Tax", "0,10 € (10%)"),
+ new("Tip", "0,10 € (10%)"),
+ new("Tax on tip", "0,01 €"),
+ new("Total", "1,21 €")
+ ]
+ });
await s.GoToUrl(editUrl);
await s.Page.ClickAsync("#TaxIncludedInPrice");
@@ -673,11 +711,12 @@ goodies:
public string ItemsTotal { get; set; }
public string Discount { get; set; }
public string Tip { get; set; }
+ public string TaxOnTip { get; set; }
}
private async Task AssertCartSummary(PlaywrightTester s, CartSummaryAssertion o)
{
- string[] ids = ["CartItemsTotal", "CartDiscount", "CartAmount", "CartTip", "CartTax", "CartTotal"];
- string[] values = [o.ItemsTotal, o.Discount, o.Subtotal, o.Tip, o.Taxes, o.Total];
+ string[] ids = ["CartItemsTotal", "CartDiscount", "CartAmount", "CartTip", "CartTaxOnTip", "CartTax", "CartTotal"];
+ string[] values = [o.ItemsTotal, o.Discount, o.Subtotal, o.Tip, o.TaxOnTip, o.Taxes, o.Total];
for (int i = 0; i < ids.Length; i++)
{
if (values[i] != null)
@@ -951,6 +990,23 @@ goodies:
]
});
+ await s.GoToUrl(editUrl);
+ await s.Page.FillAsync("#TipTaxRate", "5");
+ await s.ClickPagePrimary();
+ await s.FindAlertMessage(partialText: "App updated");
+
+ await s.GoToUrl(keypadUrl);
+ await EnterKeypad(s, "500");
+ await Expect(s.Page.Locator("#Amount")).ToContainTextAsync("5,00");
+ await s.Page.ClickAsync("label[for='ModeTablist-tip']");
+ await s.Page.ClickAsync("#Tip-10");
+ await s.Page.ClickAsync("label[for='ModeTablist-amounts']");
+ await AssertKeypadCalculation(s, "5,00 € + 0,50 € (10%) + 0,50 € (10% tax) + 0,03 € (5% tip tax)", "6,03 €");
+
+ await s.Page.ClickAsync("#pay-button");
+ await s.Page.WaitForSelectorAsync("#Checkout");
+ await s.PayInvoice(true);
+
// Guest user can access recent transactions
await s.GoToHome();
await s.Logout();
diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
index 2bdedff..b0e8bcf 100644
--- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
@@ -138,6 +138,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
DefaultTaxRate = settings.DefaultTaxRate,
+ TipTaxRate = settings.TipTaxRate,
TaxIncludedInPrice = settings.TaxIncludedInPrice,
AppId = appId,
StoreId = store.Id,
@@ -295,6 +296,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
order.AddDiscountRate(d);
if (tip is { } t)
order.AddTip(t);
+ order.SetTipTaxRate(settings.TipTaxRate);
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
@@ -377,7 +379,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
ItemCode = selectedChoices is [{} c1] ? c1.Id : null,
ItemDesc = selectedChoices is [{} c2] ? c2.Title : null,
BuyerEmail = email,
- TaxIncluded = summary.Tax == 0m ? null : summary.Tax,
+ TaxIncluded = (summary.Tax - summary.TaxOnTip) <= 0m ? null : (summary.Tax - summary.TaxOnTip),
+ TaxOnTip = summary.TaxOnTip == 0m ? null : summary.TaxOnTip,
OrderId = orderId ?? AppService.GetRandomOrderId(),
OrderUrl = Request.GetDisplayUrl(),
PosData = JObject.FromObject(jposData),
@@ -584,6 +587,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppName = app.Name,
Title = settings.Title,
DefaultTaxRate = settings.DefaultTaxRate,
+ TipTaxRate = settings.TipTaxRate,
TaxIncludedInPrice = settings.TaxIncludedInPrice,
DefaultView = settings.DefaultView,
ShowItems = settings.ShowItems,
@@ -682,6 +686,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
Title = vm.Title,
DefaultView = vm.DefaultView,
DefaultTaxRate = vm.DefaultTaxRate ?? 0,
+ TipTaxRate = vm.TipTaxRate ?? 0,
TaxIncludedInPrice = vm.TaxIncludedInPrice,
ShowItems = vm.ShowItems,
ShowCustomAmount = vm.ShowCustomAmount,
diff --git a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
index e17515e..424a239 100644
--- a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
@@ -45,6 +45,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
[Range(0.0, 100.0)]
[DisplayFormat(DataFormatString = "{0:0.00####}", ApplyFormatInEditMode = true)]
public decimal? DefaultTaxRate { get; set; }
+ [Display(Name = "Tip tax rate")]
+ [Range(0.0, 100.0)]
+ [DisplayFormat(DataFormatString = "{0:0.00####}", ApplyFormatInEditMode = true)]
+ public decimal? TipTaxRate { get; set; }
[Display(Name = "Tax included in price")]
public bool TaxIncludedInPrice { get; set; }
public string Example1 { get; internal set; }
diff --git a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs
index 163cbe7..30c72af 100644
--- a/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Models/ViewPointOfSaleViewModel.cs
@@ -86,6 +86,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
public string StoreId { get; set; }
public bool TaxIncludedInPrice { get; set; }
public decimal DefaultTaxRate { get; set; }
+ public decimal? TipTaxRate { get; set; }
public string NotAvailable { get; set; }
}
}
diff --git a/BTCPayServer/Plugins/PointOfSale/PoSOrder.cs b/BTCPayServer/Plugins/PointOfSale/PoSOrder.cs
index 36c2d4e..d298976 100644
--- a/BTCPayServer/Plugins/PointOfSale/PoSOrder.cs
+++ b/BTCPayServer/Plugins/PointOfSale/PoSOrder.cs
@@ -10,6 +10,7 @@ public class PoSOrder
private readonly int _decimals;
decimal _discount;
decimal _tip;
+ decimal? _tipTaxRate;
public List<ItemLine> ItemLines = new();
public PoSOrder(int decimals)
@@ -27,6 +28,7 @@ public class PoSOrder
{
public decimal Discount { get; set; }
public decimal Tax { get; set; }
+ public decimal TaxOnTip { get; set; }
public decimal ItemsTotal { get; set; }
public decimal PriceTaxExcluded { get; set; }
public decimal Tip { get; set; }
@@ -62,9 +64,14 @@ public class PoSOrder
}
ctx.PriceTaxExcluded = Round(ctx.PriceTaxExcluded);
ctx.PriceTaxIncluded = ctx.PriceTaxExcluded + ctx.Tax;
- ctx.PriceTaxIncludedWithTips = ctx.PriceTaxIncluded + _tip;
- ctx.PriceTaxIncludedWithTips = Round(ctx.PriceTaxIncludedWithTips);
ctx.Tip = Round(_tip);
+ if (_tipTaxRate is { } rate && rate > 0 && ctx.Tip > 0)
+ {
+ ctx.TaxOnTip = Round(ctx.Tip * rate / 100.0m);
+ ctx.Tax += ctx.TaxOnTip;
+ }
+ ctx.PriceTaxIncludedWithTips = ctx.PriceTaxIncluded + ctx.Tip + ctx.TaxOnTip;
+ ctx.PriceTaxIncludedWithTips = Round(ctx.PriceTaxIncludedWithTips);
ctx.ItemsTotal = ctx.PriceTaxExcluded + ctx.Discount;
return ctx;
}
@@ -76,6 +83,18 @@ public class PoSOrder
_tip = Round(tip);
}
+ public void SetTipTaxRate(decimal? tipTaxRate)
+ {
+ if (tipTaxRate is < 0m)
+ throw new ArgumentOutOfRangeException(nameof(tipTaxRate), "Tip tax rate cannot be negative.");
+
+ _tipTaxRate = tipTaxRate;
+ }
+ public decimal? GetTipTaxRate()
+ {
+ return _tipTaxRate;
+ }
+
/// <summary>
/// Returns the tax rate of the items in the cart.
/// If the tax rates are not all the same, returns null.
diff --git a/BTCPayServer/Plugins/PointOfSale/Views/Public/Cart.cshtml b/BTCPayServer/Plugins/PointOfSale/Views/Public/Cart.cshtml
index 3c410f2..9c1d7bd 100644
--- a/BTCPayServer/Plugins/PointOfSale/Views/Public/Cart.cshtml
+++ b/BTCPayServer/Plugins/PointOfSale/Views/Public/Cart.cshtml
@@ -203,10 +203,16 @@
<span>{{ formatCurrency(tipNumeric, true) }}</span> <span v-if="tipPercent">({{tipPercent}}%)</span>
</td>
</tr>
- <tr v-if="taxNumeric">
+ <tr v-if="summary.taxOnTip">
+ <td class="align-middle" text-translate="true">Tax on tip</td>
+ <td class="align-middle text-end" id="CartTaxOnTip">
+ {{ formatCurrency(summary.taxOnTip, true) }}
+ </td>
+ </tr>
+ <tr v-if="itemTaxNumeric">
<td class="align-middle" text-translate="true">Taxes</td>
<td class="align-middle text-end" id="CartTax">
- <span>{{ formatCurrency(taxNumeric, true) }}</span> <span v-if="taxPercent">({{taxPercent}}%)</span>
+ <span>{{ formatCurrency(itemTaxNumeric, true) }}</span> <span v-if="taxPercent">({{taxPercent}}%)</span>
</td>
</tr>
<tr>
diff --git a/BTCPayServer/Plugins/PointOfSale/Views/UpdatePointOfSale.cshtml b/BTCPayServer/Plugins/PointOfSale/Views/UpdatePointOfSale.cshtml
index 70f53f4..87f1a43 100644
--- a/BTCPayServer/Plugins/PointOfSale/Views/UpdatePointOfSale.cshtml
+++ b/BTCPayServer/Plugins/PointOfSale/Views/UpdatePointOfSale.cshtml
@@ -163,6 +163,15 @@
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div>
+ <div class="form-group">
+ <label asp-for="TipTaxRate" class="form-label"></label>
+ <div class="input-group">
+ <input inputmode="decimal" asp-for="TipTaxRate" class="form-control" />
+ <span class="input-group-text">%</span>
+ </div>
+ <div class="form-text" text-translate="true">Leave blank to not apply tax to tips.</div>
+ <span asp-validation-for="TipTaxRate" class="text-danger"></span>
+ </div>
</div>
</fieldset>
diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
index e88aa6d..8275f33 100644
--- a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
+++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
@@ -86,6 +86,8 @@ namespace BTCPayServer.Services.Apps
public bool EnableShoppingCart { get; set; }
[JsonConverter(typeof(JsonConverters.NumericStringJsonConverter))]
public decimal DefaultTaxRate { get; set; }
+ [JsonConverter(typeof(JsonConverters.NumericStringJsonConverter))]
+ public decimal TipTaxRate { get; set; }
public bool TaxIncludedInPrice { get; set; }
public PosViewType DefaultView { get; set; }
public bool ShowItems { get; set; }
diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs
index 7cc12ac..8f5bc39 100644
--- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs
+++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs
@@ -140,6 +140,12 @@ namespace BTCPayServer.Services.Invoices
get => this.GetAdditionalData<decimal?>("taxIncluded");
set => this.SetAdditionalData("taxIncluded", value);
}
+ [JsonIgnore]
+ public decimal? TaxOnTip
+ {
+ get => this.GetAdditionalData<decimal?>("taxOnTip");
+ set => this.SetAdditionalData("taxOnTip", value);
+ }
/// <summary>
/// posData is a field that may be treated differently for presentation and in some legacy API
diff --git a/BTCPayServer/Services/Invoices/PosReceiptData.cs b/BTCPayServer/Services/Invoices/PosReceiptData.cs
index 9d63186..1a12ea9 100644
--- a/BTCPayServer/Services/Invoices/PosReceiptData.cs
+++ b/BTCPayServer/Services/Invoices/PosReceiptData.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.Models;
using BTCPayServer.Plugins.PointOfSale;
@@ -15,6 +15,7 @@ public class PosReceiptData
public string Subtotal { get; set; }
public string Discount { get; set; }
public string Tip { get; set; }
+ public string TaxOnTip { get; set; }
public string Total { get; set; }
public string ItemsTotal { get; set; }
public string Tax { get; set; }
@@ -37,10 +38,21 @@ public class PosReceiptData
if (summary.Tax > 0)
{
- var taxFormatted = displayFormatter.Currency(summary.Tax, currency, DisplayFormatter.CurrencyFormat.Symbol);
- if (order.GetTaxRate() is { } r)
- taxFormatted = $"{taxFormatted} ({r:0.######}%)";
- Tax = taxFormatted;
+ var itemTax = summary.Tax - summary.TaxOnTip;
+ if (itemTax > 0)
+ {
+ var taxFormatted = displayFormatter.Currency(itemTax, currency, DisplayFormatter.CurrencyFormat.Symbol);
+ if (order.GetTaxRate() is { } r)
+ taxFormatted = $"{taxFormatted} ({r:0.######}%)";
+ Tax = taxFormatted;
+ }
+ if (summary.TaxOnTip > 0)
+ {
+ var taxOnTipFormatted = displayFormatter.Currency(summary.TaxOnTip, currency, DisplayFormatter.CurrencyFormat.Symbol);
+ if (order.GetTipTaxRate() is { } tipRate)
+ taxOnTipFormatted = $"{taxOnTipFormatted} ({tipRate:0.######}%)";
+ TaxOnTip = taxOnTipFormatted;
+ }
}
if (summary.ItemsTotal > 0)
diff --git a/BTCPayServer/Services/Invoices/WellKnownPosData.cs b/BTCPayServer/Services/Invoices/WellKnownPosData.cs
index da27e04..021ef55 100644
--- a/BTCPayServer/Services/Invoices/WellKnownPosData.cs
+++ b/BTCPayServer/Services/Invoices/WellKnownPosData.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Invoices;
@@ -17,10 +17,11 @@ public class WellKnownPosData
}
public static bool IsWellKnown(string field)
- => field.ToLowerInvariant() is "cart" or "subtotal" or "discount" or "tip" or "total" or "tax" or "itemstotal" or "discountamount";
+ => field.ToLowerInvariant() is "cart" or "subtotal" or "discount" or "tip" or "total" or "tax" or "itemstotal" or "discountamount" or "taxontip";
public object Subtotal { get; set; }
public object Discount { get; set; }
public object Tip { get; set; }
+ public object TaxOnTip { get; set; }
public object Total { get; set; }
public object ItemsTotal { get; set; }
public object Tax { get; set; }
diff --git a/BTCPayServer/Views/Shared/PosData.cshtml b/BTCPayServer/Views/Shared/PosData.cshtml
index 6a0123e..db6a689 100644
--- a/BTCPayServer/Views/Shared/PosData.cshtml
+++ b/BTCPayServer/Views/Shared/PosData.cshtml
@@ -66,6 +66,13 @@
<td class="text-end">@posData.Tip</td>
</tr>
}
+ @if (posData.TaxOnTip != null)
+ {
+ <tr>
+ <th text-translate="true">Tax on tip</th>
+ <td class="text-end">@posData.TaxOnTip</td>
+ </tr>
+ }
@if (posData.Total != null)
{
<tr style="border-top-width:3px">
diff --git a/BTCPayServer/Views/UIInvoice/Invoice.cshtml b/BTCPayServer/Views/UIInvoice/Invoice.cshtml
index 819f68b..d73a337 100644
--- a/BTCPayServer/Views/UIInvoice/Invoice.cshtml
+++ b/BTCPayServer/Views/UIInvoice/Invoice.cshtml
@@ -308,8 +308,7 @@
</div>
<div class="d-flex flex-column gap-5">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode) ||
- !string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc) ||
- Model.TypedMetadata.TaxIncluded is not null)
+ !string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<div>
<h3 class="mb-3">
@@ -333,13 +332,6 @@
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
- @if (Model.TaxIncluded is not null)
- {
- <tr>
- <th>Tax Included</th>
- <td>@Model.TaxIncluded</td>
- </tr>
- }
</table>
</div>
}
diff --git a/BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml b/BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml
index 452dd9f..1d2abcf 100644
--- a/BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml
+++ b/BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml
@@ -171,6 +171,13 @@
<td class="val text-end">@posData.Tip</td>
</tr>
}
+ @if (posData.TaxOnTip != null)
+ {
+ <tr class="sums-data">
+ <td class="key text-secondary">Tax on tip</td>
+ <td class="val text-end">@posData.TaxOnTip</td>
+ </tr>
+ }
@if (posData.Total != null)
{
<tr>
diff --git a/BTCPayServer/wwwroot/pos/common.js b/BTCPayServer/wwwroot/pos/common.js
index c3113a5..859a23c 100644
--- a/BTCPayServer/wwwroot/pos/common.js
+++ b/BTCPayServer/wwwroot/pos/common.js
@@ -6,6 +6,7 @@ class PoSOrder {
this._discount = 0;
this._tip = 0;
this._tipPercent = 0;
+ this._tipTaxRate = null;
this.itemLines = [];
}
@@ -31,6 +32,9 @@ class PoSOrder {
this._tipPercent = tip;
this._tip = 0;
}
+ setTipTaxRate(tipTaxRate) {
+ this._tipTaxRate = tipTaxRate != null ? +(tipTaxRate) : null;
+ }
addDiscountRate(discount) {
this._discount = discount;
@@ -73,6 +77,7 @@ class PoSOrder {
itemsTotal: 0,
priceTaxExcluded: 0,
tip: 0,
+ taxOnTip: 0,
priceTaxIncluded: 0,
priceTaxIncludedWithTips: 0
};
@@ -101,7 +106,11 @@ class PoSOrder {
ctx.tip = this._round(this._tip);
ctx.tip += this._round(ctx.priceTaxExcluded * this._tipPercent / 100);
ctx.priceTaxIncluded = ctx.priceTaxExcluded + ctx.tax;
- ctx.priceTaxIncludedWithTips = ctx.priceTaxIncluded + ctx.tip;
+ if (this._tipTaxRate != null && this._tipTaxRate > 0 && ctx.tip > 0) {
+ ctx.taxOnTip = this._round(ctx.tip * this._tipTaxRate / 100);
+ ctx.tax += ctx.taxOnTip;
+ }
+ ctx.priceTaxIncludedWithTips = ctx.priceTaxIncluded + ctx.tip + ctx.taxOnTip;
ctx.priceTaxIncludedWithTips = this._round(ctx.priceTaxIncludedWithTips);
ctx.itemsTotal = ctx.priceTaxExcluded + ctx.discount;
@@ -181,6 +190,9 @@ const posCommon = {
taxNumeric() {
return this.summary.tax;
},
+ itemTaxNumeric() {
+ return this.summary.tax - (this.summary.taxOnTip || 0);
+ },
taxPercent() {
return this.posOrder.getTaxRate();
},
@@ -415,6 +427,7 @@ const posCommon = {
if (this.persistState) {
this.cart = loadState('cart');
}
+ this.posOrder.setTipTaxRate(this.tipTaxRate);
this.posOrder.setCart(this.cart, this.amounts, this.defaultTaxRate, this.taxIncludedInPrice);
this.items.forEach(item => {
diff --git a/BTCPayServer/wwwroot/pos/keypad.js b/BTCPayServer/wwwroot/pos/keypad.js
index 56dc325..35b8ca3 100644
--- a/BTCPayServer/wwwroot/pos/keypad.js
+++ b/BTCPayServer/wwwroot/pos/keypad.js
@@ -42,13 +42,23 @@ document.addEventListener("DOMContentLoaded",function () {
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
if (this.summary.tax)
{
- if (this.taxIncludedInPrice) {
- const rateLabel = this.posOrder.getTaxRate() ? ` ${this.posOrder.getTaxRate()}%` : ''
- calc += ` (incl. ${this.formatCurrency(this.summary.tax, true)} tax${rateLabel})`
- } else {
- calc += ` + ${this.formatCurrency(this.summary.tax, true)}`
- if (this.posOrder.getTaxRate())
- calc += ` (${this.posOrder.getTaxRate()}% tax)`
+ const taxOnTip = this.summary.taxOnTip || 0;
+ const itemTax = this.summary.tax - taxOnTip;
+ if (itemTax > 0) {
+ if (this.taxIncludedInPrice) {
+ const rateLabel = this.posOrder.getTaxRate() ? ` ${this.posOrder.getTaxRate()}%` : ''
+ calc += ` (incl. ${this.formatCurrency(itemTax, true)} tax${rateLabel})`
+ } else {
+ calc += ` + ${this.formatCurrency(itemTax, true)}`
+ if (this.posOrder.getTaxRate())
+ calc += ` (${this.posOrder.getTaxRate()}% tax)`
+ }
+ }
+ if (taxOnTip > 0) {
+ if (this.tipTaxRate)
+ calc += ` + ${this.formatCurrency(taxOnTip, true)} (${this.tipTaxRate}% tip tax)`
+ else
+ calc += ` + ${this.formatCurrency(taxOnTip, true)} (tip tax)`
}
}
return calc
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.