mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Receipt improvements (#5239)
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user