Receipt improvements (#5239)

This commit is contained in:
d11n
2023-08-10 14:57:54 +03:00
committed by GitHub
parent 0ccbaf4bd6
commit b5d0188f21
7 changed files with 135 additions and 100 deletions

View File

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

View File

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

View File

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

View File

@@ -1,77 +1,111 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model (Dictionary<string, object> Items, int Level) @model (Dictionary<string, object> Items, int Level)
@functions { @functions {
private bool IsValidURL(string source) private bool IsValidURL(string source)
{ {
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) && return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
} }
} }
@if (Model.Items.Count > 0) @if (Model.Items.Any())
{ {
<table class="table my-0" v-pre> <table class="table my-0" v-pre>
@foreach (var (key, value) in Model.Items) @if (Model.Items.ContainsKey("Cart"))
{ {
<tr> <tbody>
@if (value is string str) @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)) else if (value is Dictionary<string, object> { Count: > 0 } subItems)
{ {
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a> <td colspan="2">
} @{
else @if (!string.IsNullOrEmpty(key))
{ {
@str.Trim() Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
} Write(key);
</td> 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))" />
<partial name="PosData" model="@((subItems, Model.Level + 1))" /> </td>
</td> }
} else if (value is IEnumerable<object> valueArray)
else if (value is IEnumerable<object> valueArray) {
{ <td colspan="2">
<td colspan="2"> @{
@{ @if (!string.IsNullOrEmpty(key))
@if (!string.IsNullOrEmpty(key)) {
{ Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">")); Write(key);
Write(key); Write(Html.Raw($"</h{Model.Level + 3}>"));
Write(Html.Raw($"</h{Model.Level + 3}>")); }
} }
} @foreach (var item in valueArray)
@foreach (var item in valueArray)
{
@if (item is Dictionary<string, object> {Count: > 0 } subItems2)
{ {
<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 </td>
{ }
<partial name="PosData" model="@((new Dictionary<string, object>() {{"", item}}, Model.Level + 1))" /> </tr>
} }
}
</td>
}
</tr>
} }
</table> </table>
} }

View File

@@ -473,7 +473,19 @@
</table> </table>
</div> </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> <div>
<h3 class="mb-3"> <h3 class="mb-3">

View File

@@ -3,9 +3,6 @@
@using BTCPayServer.Client.Models @using BTCPayServer.Client.Models
@using BTCPayServer.Components.QRCode @using BTCPayServer.Components.QRCode
@using BTCPayServer.Services @using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Payments
@inject BTCPayServerEnvironment Env @inject BTCPayServerEnvironment Env
@inject DisplayFormatter DisplayFormatter @inject DisplayFormatter DisplayFormatter
@{ @{
@@ -84,25 +81,7 @@
{ {
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<dd class="text-muted mb-0 fw-semibold">Order ID</dd> <dd class="text-muted mb-0 fw-semibold">Order ID</dd>
<dt class="fs-5 mb-0 text-break fw-semibold"> <dt class="fs-5 mb-0 text-break fw-semibold">@Model.OrderId</dt>
@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>
</div> </div>
} }
</dl> </dl>
@@ -115,6 +94,15 @@
} }
else if (isSettled) 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) if (Model.Payments?.Any() is true)
{ {
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded"> <div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
@@ -178,15 +166,10 @@
</div> </div>
</div> </div>
} }
if (Model.AdditionalData?.Any() is true) }
{ @if (!string.IsNullOrEmpty(Model.OrderUrl))
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded"> {
<h2 class="h4 mb-3">Additional Data</h2> <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 class="table-responsive my-0">
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
</div>
</div>
}
} }
</div> </div>
</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)) return parseFloat(this.cart.reduce((res, item) => res + (item.price||0) * item.count, 0).toFixed(this.currencyInfo.divisibility))
}, },
posdata () { posdata () {
const data = { const data = { cart: this.cart, subTotal: this.amountNumeric }
cart: this.cart,
subTotal: this.amountNumeric,
total: this.totalNumeric
}
if (this.tipNumeric > 0) data.tip = this.tipNumeric
if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric
if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric
if (this.tipNumeric > 0) data.tip = this.tipNumeric
data.total = this.totalNumeric
return JSON.stringify(data) return JSON.stringify(data)
} }
}, },