mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Apply taxes and set receipt properly for PoS Print View
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@@ -13,8 +14,10 @@ using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Playwright;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@@ -477,20 +480,19 @@ goodies:
|
||||
|
||||
[Fact]
|
||||
[Trait("Playwright", "Playwright")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePOSProductList()
|
||||
{
|
||||
await using var s = CreatePlaywrightTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.RegisterNewUser();
|
||||
s.AsTestAccount();
|
||||
await s.GoToHome();
|
||||
await s.RegisterNewUser(true);
|
||||
await s.CreateNewStore();
|
||||
await s.GoToStore();
|
||||
await s.AddDerivationScheme();
|
||||
await s.AddLightningNode();
|
||||
|
||||
// Let's check Custom amount works as expected
|
||||
await s.CreateApp("PointOfSale");
|
||||
var (_, appId) = await s.CreateApp("PointOfSale");
|
||||
var appUrl = s.Page.Url;
|
||||
await s.Page.FillAsync("#Currency", "BTC");
|
||||
await s.Page.SetCheckedAsync("#ShowCustomAmount", true);
|
||||
@@ -538,6 +540,41 @@ goodies:
|
||||
await s.ClickPagePrimary();
|
||||
await AssertInvoiceAmount(s, "1.50000000 BTC");
|
||||
}
|
||||
|
||||
await s.Page.FillAsync("#DefaultTaxRate", "10");
|
||||
await s.ClickPagePrimary();
|
||||
|
||||
await GoToLNUrlCheckout(s, appId, "black-tea");
|
||||
await AssertInvoiceAmount(s, "1.10000000 BTC");
|
||||
await s.MarkAsSettled();
|
||||
|
||||
await s.Page.ClickAsync("#ReceiptLink");
|
||||
await AssertReceipt(s, new()
|
||||
{
|
||||
Items = [
|
||||
new("BlackTea", "1.00000000 BTC"),
|
||||
],
|
||||
Sums = [
|
||||
new("Subtotal", "1.00000000 BTC"),
|
||||
new("Tax", "0.10000000 BTC (10%)"),
|
||||
new("Total", "1.10000000 BTC")
|
||||
]
|
||||
});
|
||||
|
||||
await GoToLNUrlCheckout(s, appId, "fruit-tea");
|
||||
await AssertInvoiceAmount(s, "Any amount");
|
||||
|
||||
await GoToLNUrlCheckout(s, appId, "");
|
||||
await AssertInvoiceAmount(s, "Any amount");
|
||||
}
|
||||
|
||||
private static async Task GoToLNUrlCheckout(PlaywrightTester s, string appId, string item)
|
||||
{
|
||||
var result = await s.Server.PayTester.HttpClient.GetStringAsync($"BTC/lnurl/pay/app/{appId}/{item}");
|
||||
var req = JsonConvert.DeserializeObject<LNURLPayRequest>(result);
|
||||
var invoiceId = Regex.Replace(req.Callback.AbsoluteUri, ".*/pay/i/(.*)", "$1");
|
||||
s.InvoiceId = invoiceId;
|
||||
await s.GoToInvoiceCheckout();
|
||||
}
|
||||
|
||||
private static async Task AssertInvoiceAmount(PlaywrightTester s, string expectedAmount)
|
||||
@@ -634,12 +671,10 @@ goodies:
|
||||
await AssertReceipt(s, new()
|
||||
{
|
||||
Items = [
|
||||
|
||||
new("Custom Amount 1", "1 234,00 €"),
|
||||
new("Custom Amount 2", "0,56 €")
|
||||
],
|
||||
Sums = [
|
||||
|
||||
new("Items total", "1 234,56 €"),
|
||||
new("Discount", "123,46 € (10%)"),
|
||||
new("Subtotal", "1 111,10 €"),
|
||||
|
||||
@@ -7,6 +7,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Blazor.VaultBridge.Elements;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
@@ -15,6 +18,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Playwright;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@@ -23,7 +27,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public Uri ServerUri;
|
||||
private string CreatedUser;
|
||||
private string InvoiceId;
|
||||
internal string InvoiceId;
|
||||
public Logging.ILog TestLogs => Server.TestLogs;
|
||||
public IPage Page { get; set; }
|
||||
public IBrowser Browser { get; private set; }
|
||||
@@ -107,9 +111,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task GoToInvoiceCheckout(string invoiceId = null)
|
||||
{
|
||||
invoiceId ??= InvoiceId;
|
||||
await Page.Locator("#StoreNav-Invoices").ClickAsync();
|
||||
await Page.Locator($"#invoice-checkout-{invoiceId}").ClickAsync();
|
||||
await Page.Locator("#Checkout").WaitForAsync();
|
||||
await GoToUrl($"/i/{invoiceId}");
|
||||
}
|
||||
|
||||
public async Task GoToWallet(WalletId walletId = null, WalletsNavPages navPages = WalletsNavPages.Send)
|
||||
@@ -380,6 +382,61 @@ namespace BTCPayServer.Tests
|
||||
await FindAlertMessage();
|
||||
}
|
||||
|
||||
public async Task AddLightningNode(string connectionType = null, bool test = true)
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
if (!(await Page.ContentAsync()).Contains("Connect to a Lightning node"))
|
||||
{
|
||||
await GoToLightningSettings();
|
||||
}
|
||||
|
||||
var connectionString = connectionType switch
|
||||
{
|
||||
LightningConnectionType.CLightning =>
|
||||
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
|
||||
LightningConnectionType.LndREST =>
|
||||
$"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (connectionString == null)
|
||||
{
|
||||
Assert.True(await Page.IsEnabledAsync("#LightningNodeType-Internal"), "Usage of the internal Lightning node is disabled.");
|
||||
await Page.ClickAsync("label[for=\"LightningNodeType-Internal\"]");
|
||||
}
|
||||
else
|
||||
{
|
||||
await Page.ClickAsync("label[for=\"LightningNodeType-Custom\"]");
|
||||
await Page.FillAsync("#ConnectionString", connectionString);
|
||||
if (test)
|
||||
{
|
||||
await Page.ClickAsync("#test");
|
||||
await FindAlertMessage(partialText: "Connection to the Lightning node successful.");
|
||||
}
|
||||
}
|
||||
|
||||
await ClickPagePrimary();
|
||||
await FindAlertMessage(partialText: $"{cryptoCode} Lightning node updated.");
|
||||
|
||||
var enabled = await Page.WaitForSelectorAsync($"#{cryptoCode}LightningEnabled");
|
||||
if (!await enabled!.IsCheckedAsync())
|
||||
{
|
||||
await enabled.ClickAsync();
|
||||
await ClickPagePrimary();
|
||||
await FindAlertMessage(partialText: $"{cryptoCode} Lightning settings successfully updated");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GoToLightningSettings(string cryptoCode = "BTC")
|
||||
{
|
||||
await Page.ClickAsync($"#StoreNav-Lightning{cryptoCode}");
|
||||
// if Lightning is already set up we need to navigate to the settings
|
||||
if ((await Page.ContentAsync()).Contains("id=\"StoreNav-LightningSettings\""))
|
||||
{
|
||||
await Page.ClickAsync("#StoreNav-LightningSettings");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClickPagePrimary()
|
||||
{
|
||||
await Page.Locator("#page-primary").ClickAsync();
|
||||
@@ -628,5 +685,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public Task SetFeeRate(decimal val) => page.FillAsync("[name=\"FeeSatoshiPerByte\"]", val.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public async Task MarkAsSettled()
|
||||
{
|
||||
var txId = Regex.Replace(Page.Url, ".*/(.*)$", "$1");
|
||||
var client = await this.AsTestAccount().CreateClient();
|
||||
await client.MarkInvoiceStatus(StoreId, txId, new() { Status = InvoiceStatus.Settled });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace BTCPayServer.Controllers
|
||||
typeof(InvoiceMetadata)
|
||||
.GetProperties()
|
||||
.Select(p => p.Name)
|
||||
.Where(p => p != "ReceiptData")
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
InvoiceAdditionalDataExclude.Remove(nameof(InvoiceMetadata.PosData));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ using BTCPayServer.Services.Stores;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Localization;
|
||||
@@ -62,6 +63,8 @@ namespace BTCPayServer
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UILNURLController(InvoiceRepository invoiceRepository,
|
||||
@@ -78,8 +81,11 @@ namespace BTCPayServer
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
IPluginHookService pluginHookService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
InvoiceActivator invoiceActivator)
|
||||
InvoiceActivator invoiceActivator,
|
||||
CurrencyNameTable currencies, DisplayFormatter displayFormatter)
|
||||
{
|
||||
_currencies = currencies;
|
||||
_displayFormatter = displayFormatter;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
@@ -264,31 +270,17 @@ namespace BTCPayServer
|
||||
return network;
|
||||
}
|
||||
|
||||
[HttpGet("pay/app/{appId}/{itemCode}")]
|
||||
[HttpGet("pay/app/{appId}/{itemCode?}")]
|
||||
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
|
||||
{
|
||||
var network = GetNetwork(cryptoCode);
|
||||
if (network is null || !network.SupportLightning)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var app = await _appService.GetApp(appId, null, true);
|
||||
if (app is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var store = app.StoreData;
|
||||
var store = app?.StoreData;
|
||||
if (store is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (network?.SupportLightning is not true ||
|
||||
GetLNUrlPaymentMethodId(cryptoCode, store, out _) is null)
|
||||
return NotFound(StringLocalizer["LNURL or LN is disabled"]);
|
||||
|
||||
AppItem[] items;
|
||||
string currencyCode;
|
||||
@@ -313,9 +305,6 @@ namespace BTCPayServer
|
||||
AppItem item = null;
|
||||
if (!string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
|
||||
if (pmi is null)
|
||||
return NotFound(StringLocalizer["LNURL or LN is disabled"]);
|
||||
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
|
||||
item = items.FirstOrDefault(item1 =>
|
||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
@@ -329,9 +318,22 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var order = new PoSOrder(_currencies.GetNumberFormatInfo(currencyCode, true).CurrencyDecimalDigits);
|
||||
|
||||
var posAppData = new PosAppData();
|
||||
posAppData.Cart = [];
|
||||
if (item != null)
|
||||
{
|
||||
posAppData.Cart = new PosAppCartItem[] { new() { Id = item.Id, Count = 1, Price = item.Price ?? 0 } };
|
||||
order.AddLine(new(item.Id ?? "", 1, item.Price ?? 0m, item.TaxRate ?? posS?.DefaultTaxRate ?? 0m));
|
||||
}
|
||||
|
||||
var summary = order.Calculate();
|
||||
var isTopup = item is null or { PriceType: AppItemPriceType.Topup } or { Price: null };
|
||||
var receiptData = PosReceiptData.Create(isTopup, item is {} ? new[]{ item } : [], posAppData, order, summary, currencyCode, _displayFormatter);
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == AppItemPriceType.Topup ? null : item?.Price,
|
||||
Amount = isTopup ? null : summary.PriceTaxIncludedWithTips,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
@@ -342,17 +344,20 @@ namespace BTCPayServer
|
||||
_ => null
|
||||
}
|
||||
},
|
||||
Metadata = new InvoiceMetadata
|
||||
{
|
||||
ItemCode = item?.Id,
|
||||
ItemDesc = item?.Title,
|
||||
TaxIncluded = summary.Tax == 0m ? null : summary.Tax,
|
||||
OrderId = AppService.GetRandomOrderId(),
|
||||
OrderUrl = Request.GetDisplayUrl(),
|
||||
PosData = JObject.FromObject(posAppData),
|
||||
ReceiptData = receiptData
|
||||
}.ToJObject(),
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not AppItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
invoiceMetadata.ItemCode = item.Id;
|
||||
invoiceMetadata.ItemDesc = item.Description;
|
||||
}
|
||||
createInvoice.Metadata = invoiceMetadata.ToJObject();
|
||||
|
||||
return await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
|
||||
@@ -321,54 +321,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
break;
|
||||
}
|
||||
|
||||
var receiptData = new PosReceiptData();
|
||||
var summary = order.Calculate();
|
||||
|
||||
var isTopup = currentView == PosViewType.Static &&
|
||||
selectedChoices.Any(c => c.PriceType == AppItemPriceType.Topup);
|
||||
if (!isTopup)
|
||||
{
|
||||
jposData.ItemsTotal = summary.ItemsTotal;
|
||||
jposData.DiscountAmount = summary.Discount;
|
||||
jposData.Subtotal = summary.PriceTaxExcluded;
|
||||
jposData.Tax = summary.Tax;
|
||||
jposData.Tip = summary.Tip;
|
||||
jposData.Total = summary.PriceTaxIncludedWithTips;
|
||||
receiptData.Subtotal = _displayFormatter.Currency(jposData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
if (jposData.DiscountAmount > 0)
|
||||
{
|
||||
var discountFormatted = _displayFormatter.Currency(jposData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.Discount = jposData.DiscountPercentage > 0 ? $"{discountFormatted} ({jposData.DiscountPercentage}%)" : discountFormatted;
|
||||
}
|
||||
|
||||
if (jposData.Tip > 0)
|
||||
{
|
||||
var tipFormatted = _displayFormatter.Currency(jposData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.Tip = jposData.TipPercentage > 0 ? $"{tipFormatted} ({jposData.TipPercentage}%)" : tipFormatted;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (jposData.ItemsTotal > 0)
|
||||
{
|
||||
var itemsTotal = _displayFormatter.Currency(jposData.ItemsTotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.ItemsTotal = itemsTotal;
|
||||
}
|
||||
|
||||
receiptData.Total = _displayFormatter.Currency(jposData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
if (receiptData.ItemsTotal == receiptData.Subtotal)
|
||||
receiptData.ItemsTotal = null;
|
||||
if (receiptData.Subtotal == receiptData.Total)
|
||||
receiptData.Subtotal = null;
|
||||
}
|
||||
|
||||
var receiptData = PosReceiptData.Create(isTopup, selectedChoices, jposData, order, summary, settings.Currency, _displayFormatter);
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest
|
||||
@@ -381,7 +338,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
ItemDesc = selectedChoices is [{} c2] ? c2.Title : null,
|
||||
BuyerEmail = email,
|
||||
TaxIncluded = summary.Tax == 0m ? null : summary.Tax,
|
||||
OrderId = orderId ?? AppService.GetRandomOrderId()
|
||||
OrderId = orderId ?? AppService.GetRandomOrderId(),
|
||||
OrderUrl = Request.GetDisplayUrl(),
|
||||
PosData = JObject.FromObject(jposData),
|
||||
ReceiptData = receiptData
|
||||
}.ToJObject(),
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
{
|
||||
@@ -400,45 +360,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl;
|
||||
entity.FullNotifications = true;
|
||||
entity.ExtendedNotifications = true;
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
entity.Metadata.PosData = JObject.FromObject(jposData);
|
||||
|
||||
if (selectedChoices.Count == 1)
|
||||
if (formResponseJObject is not null)
|
||||
{
|
||||
receiptData.Title = selectedChoices[0].Title;
|
||||
if (!string.IsNullOrEmpty(selectedChoices[0].Description))
|
||||
receiptData.Description = selectedChoices[0].Description;
|
||||
var meta = entity.Metadata.ToJObject();
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
}
|
||||
|
||||
Dictionary<string,string> cartData = null;
|
||||
foreach (var cartItem in jposData.Cart)
|
||||
{
|
||||
var selectedChoice = choices.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (selectedChoice is null)
|
||||
continue;
|
||||
var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var ident = selectedChoice.Title ?? selectedChoice.Id;
|
||||
var key = selectedChoice.PriceType == AppItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData ??= new();
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
|
||||
for (var i = 0; i < (jposData.Amounts ?? []).Length; i++)
|
||||
{
|
||||
cartData ??= new();
|
||||
cartData.Add($"Custom Amount {i + 1}", _displayFormatter.Currency(jposData.Amounts[i], settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
|
||||
receiptData.Cart = cartData;
|
||||
|
||||
entity.Metadata.SetAdditionalData("receiptData", receiptData);
|
||||
|
||||
if (formResponseJObject is null)
|
||||
return;
|
||||
var meta = entity.Metadata.ToJObject();
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
});
|
||||
var data = new { invoiceId = invoice.Id };
|
||||
if (wantsJson)
|
||||
|
||||
@@ -212,6 +212,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public PosReceiptData ReceiptData
|
||||
{
|
||||
get => this.GetAdditionalData<PosReceiptData>("receiptData");
|
||||
set => this.SetAdditionalData("receiptData", value);
|
||||
}
|
||||
|
||||
public static InvoiceMetadata FromJObject(JObject jObject)
|
||||
{
|
||||
return jObject.ToObject<InvoiceMetadata>(MetadataSerializer);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@@ -58,6 +59,16 @@ public class PosAppData
|
||||
public decimal Tip { get; set; }
|
||||
[JsonProperty(PropertyName = "total")]
|
||||
public decimal Total { get; set; }
|
||||
|
||||
internal void UpdateFrom(PoSOrder.OrderSummary summary)
|
||||
{
|
||||
ItemsTotal = summary.ItemsTotal;
|
||||
DiscountAmount = summary.Discount;
|
||||
Subtotal = summary.PriceTaxExcluded;
|
||||
Tax = summary.Tax;
|
||||
Tip = summary.Tip;
|
||||
Total = summary.PriceTaxIncludedWithTips;
|
||||
}
|
||||
}
|
||||
|
||||
public class PosAppCartItem
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@@ -16,4 +19,83 @@ public class PosReceiptData
|
||||
public string Total { get; set; }
|
||||
public string ItemsTotal { get; set; }
|
||||
public string Tax { get; set; }
|
||||
|
||||
void UpdateTotals(PosAppData appData, PoSOrder order, PoSOrder.OrderSummary summary, string currency, DisplayFormatter displayFormatter)
|
||||
{
|
||||
Subtotal = displayFormatter.Currency(summary.PriceTaxExcluded, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
|
||||
if (summary.Discount > 0)
|
||||
{
|
||||
var discountFormatted = displayFormatter.Currency(summary.Discount, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
Discount = appData.DiscountPercentage > 0 ? $"{discountFormatted} ({appData.DiscountPercentage}%)" : discountFormatted;
|
||||
}
|
||||
|
||||
if (summary.Tip > 0)
|
||||
{
|
||||
var tipFormatted = displayFormatter.Currency(summary.Tip, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
Tip = appData.TipPercentage > 0 ? $"{tipFormatted} ({appData.TipPercentage}%)" : tipFormatted;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (summary.ItemsTotal > 0)
|
||||
{
|
||||
var itemsTotal = displayFormatter.Currency(summary.ItemsTotal, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
ItemsTotal = itemsTotal;
|
||||
}
|
||||
|
||||
Total = displayFormatter.Currency(summary.PriceTaxIncludedWithTips, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
if (ItemsTotal == Subtotal)
|
||||
ItemsTotal = null;
|
||||
if (Subtotal == Total)
|
||||
Subtotal = null;
|
||||
}
|
||||
|
||||
void UpdateFromCart(IEnumerable<AppItem> appItems, PosAppData jposData, string currency, DisplayFormatter displayFormatter)
|
||||
{
|
||||
Dictionary<string,string> cartData = new();
|
||||
foreach (var cartItem in jposData.Cart ?? [])
|
||||
{
|
||||
var selectedChoice = appItems.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (selectedChoice is null)
|
||||
continue;
|
||||
if (jposData.Cart.Length == 1)
|
||||
{
|
||||
Title = selectedChoice.Title;
|
||||
if (!string.IsNullOrEmpty(selectedChoice.Description))
|
||||
Description = selectedChoice.Description;
|
||||
}
|
||||
|
||||
var singlePrice = displayFormatter.Currency(cartItem.Price, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var totalPrice = displayFormatter.Currency(cartItem.Price * cartItem.Count, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var ident = selectedChoice.Title ?? selectedChoice.Id;
|
||||
var key = selectedChoice.PriceType == AppItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
|
||||
for (var i = 0; i < (jposData.Amounts ?? []).Length; i++)
|
||||
{
|
||||
cartData.Add($"Custom Amount {i + 1}", displayFormatter.Currency(jposData.Amounts[i], currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
|
||||
Cart = cartData.Count > 0 ? cartData : null;
|
||||
}
|
||||
|
||||
public static PosReceiptData Create(bool isTopup, IEnumerable<AppItem> choices, PosAppData jposData, PoSOrder order, PoSOrder.OrderSummary summary, string currency, DisplayFormatter displayFormatter)
|
||||
{
|
||||
var receiptData = new PosReceiptData();
|
||||
if (!isTopup)
|
||||
{
|
||||
jposData.UpdateFrom(summary);
|
||||
receiptData.UpdateTotals(jposData, order, summary, currency, displayFormatter);
|
||||
}
|
||||
receiptData.UpdateFromCart(choices, jposData, currency, displayFormatter);
|
||||
return receiptData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BTC/@EntryIndexedValue">BTC</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CPFP/@EntryIndexedValue">CPFP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HWI/@EntryIndexedValue">HWI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LN/@EntryIndexedValue">LN</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LNURL/@EntryIndexedValue">LNURL</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NB/@EntryIndexedValue">NBX</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NBXplorer/@EntryIndexedValue">NBXplorer</s:String>
|
||||
|
||||
Reference in New Issue
Block a user