Public Invoice receipt (#3612)

* Public Invoice receipt

* implement payment,s qr, better ui, and fix invoice bug

* General view updates

* Update admin details link

* Update view

* add missing check

* Refactor

* make payments and qr  shown by default
* move cusotmization options to own ReceiptOptions
* Make sure to sanitize values inside PosData partial

* Refactor

* Make sure that ReceiptOptions for the StoreData is never null, and that values are always set in API

* add receipt link to checkout and add tests

* add receipt  link to lnurl

* Use ReceiptOptions.Merge

* fix lnurl

* fix chrome

* remove i18n parameterization

* Fix swagger

* Update translations

* Fix warning

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri
2022-07-06 14:14:55 +02:00
committed by GitHub
parent 2a190d579c
commit 3576ebd14f
71 changed files with 785 additions and 199 deletions

View File

@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -17,6 +18,7 @@ using BTCPayServer.Filters;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Apps;
@@ -26,6 +28,7 @@ using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitpayClient;
@@ -83,7 +86,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet("invoices/{invoiceId}")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
@@ -101,7 +104,8 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (store == null)
return NotFound();
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions);
var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel
{
@@ -133,6 +137,7 @@ namespace BTCPayServer.Controllers
CanRefund = CanRefund(invoiceState),
Refunds = invoice.Refunds,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId))
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
.ToList(),
@@ -146,7 +151,85 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet("i/{invoiceId}/receipt")]
public async Task<IActionResult> InvoiceReceipt(string invoiceId)
{
var i = await _InvoiceRepository.GetInvoice(invoiceId);
if (i is null)
return NotFound();
var store = await _StoreRepository.GetStoreByInvoiceId(i.Id);
if (store is null)
return NotFound();
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, i.ReceiptOptions);
if (receipt.Enabled is not true) return NotFound();
if (i.Status.ToModernStatus() != InvoiceStatus.Settled)
{
return View(new InvoiceReceiptViewModel
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
StoreName = store.StoreName,
Status = i.Status.ToModernStatus()
});
}
JToken? receiptData = null;
i.Metadata?.AdditionalData.TryGetValue("receiptData", out receiptData);
return View(new InvoiceReceiptViewModel
{
StoreName = store.StoreName,
Status = i.Status.ToModernStatus(),
Amount = i.Price,
Currency = i.Currency,
Timestamp = i.InvoiceTime,
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
Payments = receipt.ShowPayments is false ? null : i.GetPayments(true).Select(paymentEntity =>
{
var paymentData = paymentEntity.GetCryptoPaymentData();
var paymentMethodId = paymentEntity.GetPaymentMethodId();
if (paymentData is null || paymentMethodId is null)
{
return null;
}
string txId = paymentData.GetPaymentId();
string? link = GetTransactionLink(paymentMethodId, txId);
var paymentMethod = i.GetPaymentMethod(paymentMethodId);
var amount = paymentData.GetValue();
var rate = paymentMethod.Rate;
var paid = (amount - paymentEntity.NetworkFee) * rate;
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency),
RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
Destination = paymentData.GetDestination()
};
})
.Where(payment => payment != null)
.ToList(),
ReceiptOptions = receipt,
AdditionalData = receiptData is null
? new Dictionary<string, object>()
: PosDataParser.ParsePosData(receiptData.ToString())
});
}
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
{
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
return network == null ? null : paymentMethodId.PaymentType.GetTransactionLink(network, txId);
}
bool CanRefund(InvoiceState invoiceState)
{
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
@@ -632,6 +715,15 @@ namespace BTCPayServer.Controllers
}
lang ??= storeBlob.DefaultLang;
var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true;
var receiptUrl = receiptEnabled? _linkGenerator.GetUriByAction(
nameof(UIInvoiceController.InvoiceReceipt),
"UIInvoice",
new {invoiceId},
Request.Scheme,
Request.Host,
Request.PathBase) : null;
var model = new PaymentModel
{
Activated = paymentMethodDetails.Activated,
@@ -657,7 +749,8 @@ namespace BTCPayServer.Controllers
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.Metadata.ItemDesc,
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
TxCount = accounting.TxRequired,
@@ -1079,25 +1172,7 @@ namespace BTCPayServer.Controllers
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
ParsePosDataItem(item, ref result);
}
}
catch
@@ -1106,6 +1181,29 @@ namespace BTCPayServer.Controllers
}
return result;
}
public static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
}
}
}
}