mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-09 00:04:20 +01:00
Merge branch 'master' of https://github.com/btcpayserver/btcpayserver
This commit is contained in:
@@ -30,10 +30,10 @@ namespace BTCPayServer.Services.Rates
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
|
||||
string name = ((JProperty)item).Name;
|
||||
var value = results[name].Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
var value = results[name].Value<decimal?>();
|
||||
if (value.HasValue)
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value.Value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
@@ -260,8 +261,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private async Task WaitIsFullySynched(CancellationToken cancellationToken)
|
||||
{
|
||||
var dashBoard = GetService<NBXplorerDashboard>();
|
||||
while (!dashBoard.IsFullySynched())
|
||||
var o = GetService<IEnumerable<ISyncSummaryProvider>>().ToArray();
|
||||
while (!o.All(d => d.AllAvailable()))
|
||||
{
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -522,23 +522,23 @@ donation:
|
||||
Assert.Equal("1.234,00", await s.Page.TextContentAsync("#Amount"));
|
||||
Assert.Equal("", await s.Page.TextContentAsync("#Calculation"));
|
||||
await EnterKeypad(s, "+56");
|
||||
Assert.Equal("1.234,56", await s.Page.TextContentAsync("#Amount"));
|
||||
Assert.Equal("0,56", await s.Page.TextContentAsync("#Amount"));
|
||||
Assert.True(await s.Page.IsEnabledAsync("#ModeTablist-discount"));
|
||||
Assert.True(await s.Page.IsEnabledAsync("#ModeTablist-tip"));
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 €");
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € = 1.234,56 €");
|
||||
|
||||
// Discount: 10%
|
||||
await s.Page.ClickAsync("label[for='ModeTablist-discount']");
|
||||
await EnterKeypad(s, "10");
|
||||
Assert.Contains("1.111,10", await s.Page.TextContentAsync("#Amount"));
|
||||
Assert.Contains("0,56", await s.Page.TextContentAsync("#Amount"));
|
||||
Assert.Contains("10% discount", await s.Page.TextContentAsync("#Discount"));
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%)");
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) = 1.111,10 €");
|
||||
|
||||
// Tip: 10%
|
||||
await s.Page.ClickAsync("label[for='ModeTablist-tip']");
|
||||
await s.Page.ClickAsync("#Tip-10");
|
||||
Assert.Contains("1.222,21", await s.Page.TextContentAsync("#Amount"));
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)");
|
||||
Assert.Contains("0,56", await s.Page.TextContentAsync("#Amount"));
|
||||
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%) = 1.222,21 €");
|
||||
|
||||
// Pay
|
||||
await s.Page.ClickAsync("#pay-button");
|
||||
@@ -580,8 +580,8 @@ donation:
|
||||
await s.Page.ClickAsync("#ItemsListOffcanvas button[data-bs-dismiss='offcanvas']");
|
||||
|
||||
await EnterKeypad(s, "123");
|
||||
Assert.Contains("4,65", await s.Page.TextContentAsync("#Amount"));
|
||||
await AssertKeypadCalculation(s, "2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 € + 0,42 € (10%)");
|
||||
Assert.Contains("1,23", await s.Page.TextContentAsync("#Amount"));
|
||||
await AssertKeypadCalculation(s, "2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 € + 0,42 € (10%) = 4,65 €");
|
||||
|
||||
// Pay
|
||||
await s.Page.ClickAsync("#pay-button");
|
||||
@@ -604,7 +604,7 @@ donation:
|
||||
],
|
||||
Sums = [
|
||||
new("Subtotal", "4,23 €"),
|
||||
new("Tax", "0,42 €"),
|
||||
new("Tax", "0,42 € (10%)"),
|
||||
new("Total", "4,65 €")
|
||||
]
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
@@ -22,7 +21,8 @@ namespace BTCPayServer.Tests
|
||||
#endif
|
||||
public static DirectoryInfo TryGetSolutionDirectoryInfo()
|
||||
{
|
||||
var directory = new DirectoryInfo(TestDirectory);
|
||||
var btcPayDirectory = ((OutputPathAttribute)typeof(TestUtils).Assembly.GetCustomAttributes(typeof(OutputPathAttribute), true)[0]).BuiltPath;
|
||||
var directory = new DirectoryInfo(btcPayDirectory);
|
||||
while (directory != null && !directory.GetFiles("*.sln").Any())
|
||||
{
|
||||
directory = directory.Parent;
|
||||
@@ -32,9 +32,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
static TestUtils()
|
||||
{
|
||||
TestDirectory = ((OutputPathAttribute)typeof(TestUtils).Assembly.GetCustomAttributes(typeof(OutputPathAttribute), true)[0]).BuiltPath;
|
||||
TestDirectory = AppContext.BaseDirectory;
|
||||
}
|
||||
public readonly static string TestDirectory;
|
||||
public static readonly string TestDirectory;
|
||||
|
||||
public static string GetTestDataFullPath(string relativeFilePath)
|
||||
{
|
||||
|
||||
@@ -138,13 +138,14 @@ namespace BTCPayServer.Tests
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.AvailableRateProviders.Where(s => s.Source == RateSource.Direct)
|
||||
.Select(s => s.Id).ToHashSet();
|
||||
foreach (var result in factory
|
||||
var providerList = factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider bf &&
|
||||
!(bf.Inner is CoinGeckoRateProvider cg && cg.UnderlyingExchange != null))
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default),
|
||||
Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.ToList())
|
||||
.ToList();
|
||||
foreach (var result in providerList)
|
||||
{
|
||||
var name = result.ExpectedName;
|
||||
if (brokenShitcoinCasinos.Contains(name))
|
||||
@@ -207,6 +208,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "LBP") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope)
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "XPT") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope)
|
||||
}
|
||||
else if (name == "bitmynt")
|
||||
{
|
||||
|
||||
@@ -345,6 +345,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (jposData.Tax > 0)
|
||||
{
|
||||
var taxFormatted = _displayFormatter.Currency(jposData.Tax, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
if (order.GetTaxRate() is { } r)
|
||||
taxFormatted = $"{taxFormatted} ({r:0.######}%)";
|
||||
receiptData.Tax = taxFormatted;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.PointOfSale;
|
||||
|
||||
@@ -64,6 +65,20 @@ public class PoSOrder
|
||||
_tip = Round(tip);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the tax rate of the items in the cart.
|
||||
/// If the tax rates are not all the same, returns null.
|
||||
/// If the cart is empty, returns null.
|
||||
/// Else, returns the tax rate shared by all items
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public decimal? GetTaxRate()
|
||||
{
|
||||
if (!ItemLines.Any())
|
||||
return null;
|
||||
return ItemLines.GroupBy(i => i.TaxRate).Count() == 1 ? ItemLines[0].TaxRate : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
@@ -191,9 +191,11 @@
|
||||
<tr v-if="showDiscount">
|
||||
<th class="align-middle" text-translate="true">Discount</th>
|
||||
<th class="align-middle" colspan="3">
|
||||
<div class="input-group input-group-sm w-100px pull-right">
|
||||
<input class="form-control hide-number-spin" type="number" inputmode="decimal" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
|
||||
<span class="input-group-text">%</span>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="input-group input-group-sm w-100px">
|
||||
<input class="form-control hide-number-spin" type="number" inputmode="decimal" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -233,7 +235,7 @@
|
||||
<tr v-if="discountNumeric">
|
||||
<td class="align-middle" text-translate="true">Discount</td>
|
||||
<td class="align-middle text-end" id="CartDiscount">
|
||||
<span>{{ formatCurrency(discountNumeric, true) }}</span> <span v-if="discountPercent">({{discountPercent}}%)</span>
|
||||
<span>{{ formatCurrency(discountNumeric, true) }}</span> <span v-if="discountPercent">({{discountPercent}}%)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="subtotalNumeric">
|
||||
@@ -243,13 +245,13 @@
|
||||
<tr v-if="tipNumeric">
|
||||
<td class="align-middle" text-translate="true">Tip</td>
|
||||
<td class="align-middle text-end" id="CartTip">
|
||||
<span>{{ formatCurrency(tipNumeric, true) }}</span> <span v-if="tipPercent">({{tipPercent}}%)</span>
|
||||
<span>{{ formatCurrency(tipNumeric, true) }}</span> <span v-if="tipPercent">({{tipPercent}}%)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxNumeric">
|
||||
<td class="align-middle" text-translate="true">Taxes</td>
|
||||
<td class="align-middle text-end" id="CartTax">
|
||||
{{ formatCurrency(taxNumeric, true) }}
|
||||
<span>{{ formatCurrency(taxNumeric, true) }}</span> <span v-if="taxPercent">({{taxPercent}}%)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<input type="hidden" name="posdata" :value="posdata">
|
||||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
||||
<div class="fw-semibold text-muted" id="Currency">{{currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(totalNumeric, false) }}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(lastAmount, false) }}</div>
|
||||
<div class="text-muted text-center mt-2" id="Calculation">{{ calculation }}</div>
|
||||
</div>
|
||||
<div id="ModeTabs" class="tab-content mb-n2" v-if="showDiscount || enableTips">
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<tfoot style="border-top-width:0">
|
||||
@if (posData.ItemsTotal != null)
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<tr>
|
||||
<th text-translate="true">Items total</th>
|
||||
<td class="text-end">@posData.ItemsTotal</td>
|
||||
</tr>
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
@if (posData.Subtotal != null)
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<tr>
|
||||
<th text-translate="true">Subtotal</th>
|
||||
<td class="text-end">@posData.Subtotal</td>
|
||||
</tr>
|
||||
|
||||
@@ -139,9 +139,6 @@
|
||||
|
||||
@if (posData.ItemsTotal != null)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
<tr class="sums-data">
|
||||
<td class="key text-secondary">Items total</td>
|
||||
<td class="val text-end">@posData.ItemsTotal</td>
|
||||
|
||||
@@ -50,6 +50,21 @@ class PoSOrder {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the tax rate of the items in the cart.
|
||||
// If the tax rates are not all the same, returns null.
|
||||
// If the cart is empty, returns null.
|
||||
// Else, returns the tax rate shared by all items
|
||||
getTaxRate() {
|
||||
if (this.itemLines.length === 0) return null;
|
||||
var rate = this.itemLines[0].taxRate ?? 0;
|
||||
for (const line of this.itemLines.slice(1)) {
|
||||
if (rate !== line.taxRate)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
calculate() {
|
||||
const ctx = {
|
||||
discount: 0,
|
||||
@@ -159,6 +174,9 @@ const posCommon = {
|
||||
taxNumeric() {
|
||||
return this.summary.tax;
|
||||
},
|
||||
taxPercent() {
|
||||
return this.posOrder.getTaxRate();
|
||||
},
|
||||
subtotalNumeric () {
|
||||
// We don't want to show the subtotal if there is no tax or tips
|
||||
if (this.summary.priceTaxExcluded === this.summary.priceTaxIncludedWithTips) return 0;
|
||||
@@ -185,6 +203,9 @@ const posCommon = {
|
||||
tipNumeric () {
|
||||
return this.summary.tip;
|
||||
},
|
||||
lastAmount() {
|
||||
return this.amounts[this.amounts.length - 1] = this.amounts[this.amounts.length - 1] || 0;
|
||||
},
|
||||
totalNumeric () {
|
||||
return this.summary.priceTaxIncludedWithTips;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
document.addEventListener("DOMContentLoaded",function () {
|
||||
|
||||
const displayFontSize = 64;
|
||||
new Vue({
|
||||
el: '#app',
|
||||
@@ -39,8 +40,15 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
if (this.discountNumeric > 0 || this.discountPercentNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
|
||||
if (this.summary.tip > 0) calc += ` + ${this.formatCurrency(this.summary.tip, true)}`
|
||||
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
|
||||
if (this.summary.tax) calc += ` + ${this.formatCurrency(this.summary.tax, true)}`
|
||||
if (this.defaultTaxRate) calc += ` (${this.defaultTaxRate}%)`
|
||||
if (this.summary.tax)
|
||||
{
|
||||
calc += ` + ${this.formatCurrency(this.summary.tax, true)}`
|
||||
if (this.posOrder.getTaxRate())
|
||||
{
|
||||
calc += ` (${this.posOrder.getTaxRate()}%)`
|
||||
}
|
||||
}
|
||||
calc += ` = ${this.formatCurrency(this.summary.priceTaxIncludedWithTips, true)}`
|
||||
return calc
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user