mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Receipt improvements (#5239)
This commit is contained in:
parent
0ccbaf4bd6
commit
b5d0188f21
7 changed files with 135 additions and 100 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue