mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user