Receipt improvements (#5239)

This commit is contained in:
d11n 2023-08-10 14:57:54 +03:00 committed by GitHub
parent 0ccbaf4bd6
commit b5d0188f21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 100 deletions

View file

@ -123,6 +123,7 @@ namespace BTCPayServer.Controllers
var additionalData = metaData
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
.ToDictionary(dict => dict.Key, dict => dict.Value);
var model = new InvoiceDetailsModel
{
StoreId = store.Id,
@ -149,7 +150,6 @@ namespace BTCPayServer.Controllers
StatusException = invoice.ExceptionStatus,
Events = invoice.Events,
Metadata = metaData,
AdditionalData = additionalData,
Archived = invoice.Archived,
CanRefund = invoiceState.CanRefund(),
Refunds = invoice.Refunds,
@ -166,6 +166,13 @@ namespace BTCPayServer.Controllers
model.CryptoPayments = details.CryptoPayments;
model.Payments = details.Payments;
model.Overpaid = details.Overpaid;
if (additionalData.ContainsKey("receiptData"))
{
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
additionalData.Remove("receiptData");
}
model.AdditionalData = additionalData;
return View(model);
}

View file

@ -127,6 +127,7 @@ namespace BTCPayServer.Models.InvoicingModels
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, object> Metadata { get; set; }
public Dictionary<string, object> ReceiptData { get; set; }
public Dictionary<string, object> AdditionalData { get; set; }
public List<PaymentEntity> Payments { get; set; }
public bool Archived { get; set; }

View file

@ -347,9 +347,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var receiptData = new JObject();
if (choice is not null)
{
receiptData = JObject.FromObject(new Dictionary<string, string>()
receiptData = JObject.FromObject(new Dictionary<string, string>
{
{"Title", choice.Title}, {"Description", choice.Description},
{"Title", choice.Title},
{"Description", choice.Description},
});
}
else if (jposData is not null)
@ -370,21 +371,21 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
var ident = selectedChoice.Title ?? selectedChoice.Id;
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
cartData.Add(key, $"{singlePrice} x {cartItem.Count} = {totalPrice}");
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
}
receiptData.Add("Cart", cartData);
}
receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
if (appPosData.DiscountAmount > 0)
{
receiptData.Add("Discount",
$"{_displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)} {(appPosData.DiscountPercentage > 0 ? $"({appPosData.DiscountPercentage}%)" : string.Empty)}");
var discountFormatted = _displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
receiptData.Add("Discount", appPosData.DiscountPercentage > 0 ? $"{appPosData.DiscountPercentage}% = {discountFormatted}" : discountFormatted);
}
if (appPosData.Tip > 0)
{
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
}
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
}
entity.Metadata.SetAdditionalData("receiptData", receiptData);

View file

@ -1,77 +1,111 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model (Dictionary<string, object> Items, int Level)
@functions {
private bool IsValidURL(string source)
{
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
}
}
@if (Model.Items.Count > 0)
@if (Model.Items.Any())
{
<table class="table my-0" v-pre>
@foreach (var (key, value) in Model.Items)
@if (Model.Items.ContainsKey("Cart"))
{
<tr>
@if (value is string str)
<tbody>
@foreach (var (key, value) in (Dictionary <string, object>)Model.Items["Cart"])
{
if (!string.IsNullOrEmpty(key))
<tr>
<td>@key</td>
<td class="text-end">@value</td>
</tr>
}
</tbody>
<tfoot style="border-top-width:3px">
@if (Model.Items.ContainsKey("Subtotal"))
{
<tr>
<td>Subtotal</td>
<td class="text-end">@Model.Items["Subtotal"]</td>
</tr>
}
@if (Model.Items.ContainsKey("Discount"))
{
<tr>
<td>Discount</td>
<td class="text-end">@Model.Items["Discount"]</td>
</tr>
}
@if (Model.Items.ContainsKey("Tip"))
{
<tr>
<td>Tip</td>
<td class="text-end">@Model.Items["Tip"]</td>
</tr>
}
@if (Model.Items.ContainsKey("Total"))
{
<tr style="border-top-width:3px">
<td>Total</td>
<td class="text-end">@Model.Items["Total"]</td>
</tr>
}
</tfoot>
}
else
{
foreach (var (key, value) in Model.Items)
{
<tr>
@if (value is string str)
{
<th class="w-150px">@key</th>
if (!string.IsNullOrEmpty(key))
{
<th class="w-225px">@key</th>
}
<td style="white-space:pre-wrap">@* Explicitely remove whitespace at front here *@@if (IsValidURL(str)){<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>}else {@str.Trim()}</td>
}
<td style="white-space:pre-wrap">@* Explicitely remove whitespace at front here *@@if (IsValidURL(str))
{
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
}
else
{
@str.Trim()
}
</td>
}
else if (value is Dictionary<string, object> {Count: > 0 } subItems)
{
<td colspan="2">
@{
@if (!string.IsNullOrEmpty(key))
{
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
Write(key);
Write(Html.Raw($"</h{Model.Level + 3}>"));
else if (value is Dictionary<string, object> { Count: > 0 } subItems)
{
<td colspan="2">
@{
@if (!string.IsNullOrEmpty(key))
{
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
Write(key);
Write(Html.Raw($"</h{Model.Level + 3}>"));
}
}
}
<partial name="PosData" model="@((subItems, Model.Level + 1))" />
</td>
}
else if (value is IEnumerable<object> valueArray)
{
<td colspan="2">
@{
@if (!string.IsNullOrEmpty(key))
{
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
Write(key);
Write(Html.Raw($"</h{Model.Level + 3}>"));
<partial name="PosData" model="@((subItems, Model.Level + 1))" />
</td>
}
else if (value is IEnumerable<object> valueArray)
{
<td colspan="2">
@{
@if (!string.IsNullOrEmpty(key))
{
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
Write(key);
Write(Html.Raw($"</h{Model.Level + 3}>"));
}
}
}
@foreach (var item in valueArray)
{
@if (item is Dictionary<string, object> {Count: > 0 } subItems2)
@foreach (var item in valueArray)
{
<partial name="PosData" model="@((subItems2, Model.Level + 1))" />
@if (item is Dictionary<string, object> { Count: > 0 } subItems2)
{
<partial name="PosData" model="@((subItems2, Model.Level + 1))" />
}
else
{
<partial name="PosData" model="@((new Dictionary<string, object> { { "", item } }, Model.Level + 1))" />
}
}
else
{
<partial name="PosData" model="@((new Dictionary<string, object>() {{"", item}}, Model.Level + 1))" />
}
}
</td>
}
</tr>
</td>
}
</tr>
}
}
</table>
}

View file

@ -473,7 +473,19 @@
</table>
</div>
}
@if (Model.AdditionalData.Any())
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
{
<div>
<h3 class="mb-3">
<span>Receipt Information</span>
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="info" />
</a>
</h3>
<partial name="PosData" model="(Model.ReceiptData, 1)" />
</div>
}
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
{
<div>
<h3 class="mb-3">

View file

@ -3,9 +3,6 @@
@using BTCPayServer.Client.Models
@using BTCPayServer.Components.QRCode
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Payments
@inject BTCPayServerEnvironment Env
@inject DisplayFormatter DisplayFormatter
@{
@ -84,25 +81,7 @@
{
<div class="d-flex flex-column">
<dd class="text-muted mb-0 fw-semibold">Order ID</dd>
<dt class="fs-5 mb-0 text-break fw-semibold">
@if (!string.IsNullOrEmpty(Model.OrderUrl))
{
<a href="@Model.OrderUrl" rel="noreferrer noopener" target="_blank">
@if (string.IsNullOrEmpty(Model.OrderId))
{
<span>View Order</span>
}
else
{
@Model.OrderId
}
</a>
}
else
{
<span>@Model.OrderId</span>
}
</dt>
<dt class="fs-5 mb-0 text-break fw-semibold">@Model.OrderId</dt>
</div>
}
</dl>
@ -115,6 +94,15 @@
}
else if (isSettled)
{
if (Model.AdditionalData?.Any() is true)
{
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
<h2 class="h4 mb-3">Additional Data</h2>
<div class="table-responsive my-0">
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
</div>
</div>
}
if (Model.Payments?.Any() is true)
{
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
@ -178,15 +166,10 @@
</div>
</div>
}
if (Model.AdditionalData?.Any() is true)
{
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
<h2 class="h4 mb-3">Additional Data</h2>
<div class="table-responsive my-0">
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
</div>
</div>
}
}
@if (!string.IsNullOrEmpty(Model.OrderUrl))
{
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
}
</div>
</div>

View file

@ -55,14 +55,11 @@ document.addEventListener("DOMContentLoaded",function () {
return parseFloat(this.cart.reduce((res, item) => res + (item.price||0) * item.count, 0).toFixed(this.currencyInfo.divisibility))
},
posdata () {
const data = {
cart: this.cart,
subTotal: this.amountNumeric,
total: this.totalNumeric
}
if (this.tipNumeric > 0) data.tip = this.tipNumeric
const data = { cart: this.cart, subTotal: this.amountNumeric }
if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric
if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric
if (this.tipNumeric > 0) data.tip = this.tipNumeric
data.total = this.totalNumeric
return JSON.stringify(data)
}
},