mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-04 09:58:13 +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:
parent
2a190d579c
commit
3576ebd14f
71 changed files with 785 additions and 199 deletions
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -19,6 +20,48 @@ namespace BTCPayServer.Client.Models
|
|||
public string Currency { get; set; }
|
||||
public JObject Metadata { get; set; }
|
||||
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
|
||||
public ReceiptOptions Receipt { get; set; } = new ReceiptOptions();
|
||||
|
||||
public class ReceiptOptions
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
public bool? ShowQR { get; set; }
|
||||
public bool? ShowPayments { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Make sure that the return has all values set by order of priority: invoice/store/default
|
||||
/// </summary>
|
||||
/// <param name="storeLevelOption"></param>
|
||||
/// <param name="invoiceLevelOption"></param>
|
||||
/// <returns></returns>
|
||||
public static ReceiptOptions Merge(ReceiptOptions? storeLevelOption, ReceiptOptions? invoiceLevelOption)
|
||||
{
|
||||
storeLevelOption ??= new ReceiptOptions();
|
||||
invoiceLevelOption ??= new ReceiptOptions();
|
||||
var store = JObject.FromObject(storeLevelOption);
|
||||
var inv = JObject.FromObject(invoiceLevelOption);
|
||||
var result = JObject.FromObject(CreateDefault());
|
||||
var mergeSettings = new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore };
|
||||
result.Merge(store, mergeSettings);
|
||||
result.Merge(inv, mergeSettings);
|
||||
var options = result.ToObject<ReceiptOptions>()!;
|
||||
return options;
|
||||
}
|
||||
|
||||
public static ReceiptOptions CreateDefault()
|
||||
{
|
||||
return new ReceiptOptions()
|
||||
{
|
||||
ShowQR = true,
|
||||
Enabled = true,
|
||||
ShowPayments = true
|
||||
};
|
||||
}
|
||||
#nullable restore
|
||||
}
|
||||
public class CheckoutOptions
|
||||
{
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ namespace BTCPayServer.Client.Models
|
|||
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
|
||||
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
|
||||
public InvoiceData.ReceiptOptions Receipt { get; set; }
|
||||
|
||||
|
||||
[JsonExtensionData]
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="103.0.5060.5300" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
@ -131,6 +131,39 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanMergeReceiptOptions()
|
||||
{
|
||||
var r = InvoiceDataBase.ReceiptOptions.Merge(null, null);
|
||||
Assert.True(r?.Enabled);
|
||||
Assert.True(r?.ShowPayments);
|
||||
Assert.True(r?.ShowQR);
|
||||
|
||||
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions(), null);
|
||||
Assert.True(r?.Enabled);
|
||||
Assert.True(r?.ShowPayments);
|
||||
Assert.True(r?.ShowQR);
|
||||
|
||||
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions() { Enabled = false }, null);
|
||||
Assert.False(r?.Enabled);
|
||||
Assert.True(r?.ShowPayments);
|
||||
Assert.True(r?.ShowQR);
|
||||
|
||||
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions() { Enabled = false, ShowQR = false }, new InvoiceDataBase.ReceiptOptions() { Enabled = true });
|
||||
Assert.True(r?.Enabled);
|
||||
Assert.True(r?.ShowPayments);
|
||||
Assert.False(r?.ShowQR);
|
||||
|
||||
StoreBlob blob = new StoreBlob();
|
||||
Assert.True(blob.ReceiptOptions.Enabled);
|
||||
blob = JsonConvert.DeserializeObject<StoreBlob>("{}");
|
||||
Assert.True(blob.ReceiptOptions.Enabled);
|
||||
blob = JsonConvert.DeserializeObject<StoreBlob>("{\"receiptOptions\":{\"enabled\": false}}");
|
||||
Assert.False(blob.ReceiptOptions.Enabled);
|
||||
blob = JsonConvert.DeserializeObject<StoreBlob>(JsonConvert.SerializeObject(blob));
|
||||
Assert.False(blob.ReceiptOptions.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParsePaymentMethodId()
|
||||
{
|
||||
|
|
|
@ -86,9 +86,19 @@ namespace BTCPayServer.Tests
|
|||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
public void PayInvoice()
|
||||
public void PayInvoice(bool mine = false)
|
||||
{
|
||||
Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
if (mine)
|
||||
{
|
||||
MineBlockOnInvoiceCheckout();
|
||||
}
|
||||
}
|
||||
|
||||
public void MineBlockOnInvoiceCheckout()
|
||||
{
|
||||
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
|
|||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
|
@ -437,6 +438,61 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseInvoiceReceipts()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
s.GoToInvoices();
|
||||
var i = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(i);
|
||||
s.PayInvoice(true);
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
|
||||
s.Driver.PageSource);
|
||||
Assert.Contains(i, s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoices(s.StoreId);
|
||||
i = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(i);
|
||||
var receipturl = s.Driver.Url + "/receipt";
|
||||
s.Driver.Navigate().GoToUrl(receipturl);
|
||||
s.Driver.FindElement(By.Id("invoice-unsettled"));
|
||||
|
||||
s.GoToInvoices(s.StoreId);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
var checkouturi = s.Driver.Url;
|
||||
s.PayInvoice();
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
|
||||
Assert.Contains("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
s.GoToUrl(checkouturi);
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanSetupStoreViaGuide()
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.22" />
|
||||
<PackageReference Include="LNURL" Version="0.0.24" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
|
|
|
@ -431,7 +431,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
}
|
||||
},
|
||||
Receipt = entity.ReceiptOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,13 +25,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
|
||||
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager, BTCPayNetworkProvider btcPayNetworkProvider)
|
||||
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores")]
|
||||
|
@ -129,6 +127,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
|
@ -166,6 +165,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ using BTCPayServer.Services.Stores;
|
|||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using NBitpayClient;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
@ -44,6 +45,7 @@ namespace BTCPayServer.Controllers
|
|||
private readonly LanguageService _languageService;
|
||||
private readonly ExplorerClientProvider _ExplorerClients;
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
|
@ -62,7 +64,8 @@ namespace BTCPayServer.Controllers
|
|||
WebhookSender webhookNotificationManager,
|
||||
LanguageService languageService,
|
||||
ExplorerClientProvider explorerClients,
|
||||
UIWalletsController walletsController)
|
||||
UIWalletsController walletsController,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
|
@ -78,6 +81,7 @@ namespace BTCPayServer.Controllers
|
|||
_languageService = languageService;
|
||||
this._ExplorerClients = explorerClients;
|
||||
_walletsController = walletsController;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
|
||||
|
@ -159,6 +163,7 @@ namespace BTCPayServer.Controllers
|
|||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
|
||||
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
||||
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken);
|
||||
}
|
||||
|
||||
|
@ -169,6 +174,7 @@ namespace BTCPayServer.Controllers
|
|||
entity.ServerUrl = serverUrl;
|
||||
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
|
||||
entity.ReceiptOptions = invoice.Receipt ?? new InvoiceDataBase.ReceiptOptions();
|
||||
if (invoice.Metadata != null)
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
|
||||
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
|
|
|
@ -342,7 +342,8 @@ namespace BTCPayServer
|
|||
return NotFound("Store not found");
|
||||
}
|
||||
|
||||
currencyCode ??= store.GetStoreBlob().DefaultCurrency ?? cryptoCode;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
currencyCode ??= storeBlob.DefaultCurrency ?? cryptoCode;
|
||||
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
|
||||
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
|
||||
|
@ -429,7 +430,6 @@ namespace BTCPayServer
|
|||
{
|
||||
lnurlMetadata.Add(new[] {"text/identifier", lnAddress});
|
||||
}
|
||||
|
||||
return Ok(new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
|
@ -513,6 +513,18 @@ namespace BTCPayServer
|
|||
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Amount is out of bounds."});
|
||||
}
|
||||
|
||||
LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null;
|
||||
|
||||
if ((i.ReceiptOptions?.Enabled ??blob.ReceiptOptions.Enabled ) is true)
|
||||
{
|
||||
successAction =
|
||||
new LNURLPayRequest.LNURLPayRequestCallbackResponse.LNURLPayRequestSuccessActionUrl()
|
||||
{
|
||||
Tag = "url",
|
||||
Description = "Thank you for your purchase. Here is your receipt",
|
||||
Url = _linkGenerator.GetUriByAction(HttpContext, "InvoiceReceipt", "UIInvoice", new { invoiceId})
|
||||
};
|
||||
}
|
||||
if (amount.HasValue && string.IsNullOrEmpty(paymentMethodDetails.BOLT11) ||
|
||||
paymentMethodDetails.GeneratedBoltAmount != amount)
|
||||
{
|
||||
|
@ -573,7 +585,8 @@ namespace BTCPayServer
|
|||
paymentMethodDetails, pmi));
|
||||
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
|
||||
{
|
||||
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
|
||||
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11,
|
||||
SuccessAction = successAction
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -588,7 +601,8 @@ namespace BTCPayServer
|
|||
|
||||
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
|
||||
{
|
||||
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
|
||||
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11,
|
||||
SuccessAction = successAction
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -385,6 +385,7 @@ namespace BTCPayServer.Controllers
|
|||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions);
|
||||
vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage;
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
|
||||
|
@ -496,6 +497,7 @@ namespace BTCPayServer.Controllers
|
|||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
blob.ReceiptOptions = model.ReceiptOptions.ToDTO();
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
|
|
|
@ -30,6 +30,7 @@ namespace BTCPayServer.Data
|
|||
ShowRecommendedFee = true;
|
||||
RecommendedFeeBlockTarget = 1;
|
||||
PaymentMethodCriteria = new List<PaymentMethodCriteria>();
|
||||
ReceiptOptions = InvoiceDataBase.ReceiptOptions.CreateDefault();
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
|
@ -104,7 +105,10 @@ namespace BTCPayServer.Data
|
|||
public bool AutoDetectLanguage { get; set; }
|
||||
|
||||
public bool RateScripting { get; set; }
|
||||
|
||||
#nullable enable
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
|
||||
#nullable restore
|
||||
public string RateScript { get; set; }
|
||||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -133,5 +134,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
public bool ShowReceipt { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using static BTCPayServer.Client.Models.InvoiceDataBase;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class InvoiceReceiptViewModel
|
||||
{
|
||||
public InvoiceStatus Status { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string StoreName { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
public ReceiptOptions ReceiptOptions { get; set; }
|
||||
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
|
||||
}
|
||||
}
|
|
@ -68,5 +68,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public bool RedirectAutomatically { get; set; }
|
||||
public bool Activated { get; set; }
|
||||
public string InvoiceCurrency { get; set; }
|
||||
public string? ReceiptLink { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
using System.Linq;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
|
@ -47,6 +48,26 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
public class ReceiptOptionsViewModel
|
||||
{
|
||||
public static ReceiptOptionsViewModel Create(Client.Models.InvoiceDataBase.ReceiptOptions opts)
|
||||
{
|
||||
return JObject.FromObject(opts).ToObject<ReceiptOptionsViewModel>();
|
||||
}
|
||||
[Display(Name = "Enable public receipt page for settled invoices")]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[Display(Name = "Show the QR code of the receipt in the public receipt page")]
|
||||
public bool ShowQR { get; set; }
|
||||
|
||||
[Display(Name = "Show the payment list in the public receipt page")]
|
||||
public bool ShowPayments { get; set; }
|
||||
public Client.Models.InvoiceDataBase.ReceiptOptions ToDTO()
|
||||
{
|
||||
return JObject.FromObject(this).ToObject<Client.Models.InvoiceDataBase.ReceiptOptions>();
|
||||
}
|
||||
}
|
||||
public ReceiptOptionsViewModel ReceiptOptions { get; set; } = ReceiptOptionsViewModel.Create(Client.Models.InvoiceDataBase.ReceiptOptions.CreateDefault());
|
||||
public List<PaymentMethodCriteriaViewModel> PaymentMethodCriteria { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
@ -442,6 +442,9 @@ namespace BTCPayServer.Services.Invoices
|
|||
public InvoiceType Type { get; set; }
|
||||
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
|
|
|
@ -204,8 +204,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="success-message">{{$t("This invoice has been paid")}}</div>
|
||||
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
|
||||
<span v-html="$t('Return to StoreName', srvModel)"></span>
|
||||
<a class="action-button" :href="srvModel.merchantRefLink" v-if="!isModal && srvModel.merchantRefLink">
|
||||
<span v-if="srvModel.receiptLink != srvModel.merchantRefLink" v-html="$t('Return to StoreName', srvModel)"></span>
|
||||
<span v-else v-html="$t('View receipt')"></span>
|
||||
</a>
|
||||
<a class="action-button" :href="srvModel.receiptLink" :target="isModal?'_blank':'_top'" v-if="srvModel.receiptLink && (srvModel.merchantRefLink != srvModel.receiptLink) || isModal">
|
||||
<span v-html="$t('View receipt')"></span>
|
||||
</a>
|
||||
<button class="action-button close-action" v-show="isModal" v-on:click="close">
|
||||
<span v-html="$t('Close')"></span>
|
||||
|
|
|
@ -90,20 +90,24 @@
|
|||
|
||||
<div class="invoice-details">
|
||||
<div class="sticky-header-setup"></div>
|
||||
<div class="sticky-header d-md-flex align-items-center justify-content-between">
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<div class="d-flex gap-3 mt-3 mt-md-0">
|
||||
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<h2 class="mb-0 text-break">@ViewData["Title"]</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
@if (Model.ShowCheckout)
|
||||
{
|
||||
<a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap" asp-route-invoiceId="@Model.Id">Checkout</a>
|
||||
}
|
||||
@if (Model.ShowReceipt)
|
||||
{
|
||||
<a asp-action="InvoiceReceipt" asp-route-invoiceId="@Model.Id" id="Receipt" class="btn btn-secondary" target="InvoiceReceipt-@Model.Id">Receipt</a>
|
||||
}
|
||||
@if (Model.CanRefund)
|
||||
{
|
||||
<a id="IssueRefund" class="btn btn-success text-nowrap" asp-action="Refund" asp-route-invoiceId="@Model.Id" data-bs-toggle="modal" data-bs-target="#RefundModal">Issue Refund</a>
|
||||
<a asp-action="Refund" asp-route-invoiceId="@Model.Id" id="IssueRefund" class="btn btn-success text-nowrap" data-bs-toggle="modal" data-bs-target="#RefundModal">Issue Refund</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button href="#" class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled>Issue refund</button>
|
||||
<button class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled>Issue refund</button>
|
||||
}
|
||||
<form asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
|
||||
<button type="submit" class="btn btn-secondary" id="btn-archive-toggle">
|
||||
|
|
170
BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml
Normal file
170
BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml
Normal file
|
@ -0,0 +1,170 @@
|
|||
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services.Rates
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@inject BTCPayServer.Services.ThemeSettings Theme
|
||||
@inject CurrencyNameTable CurrencyNameTable
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = $"Receipt from {Model.StoreName}";
|
||||
var isProcessing = Model.Status == InvoiceStatus.Processing;
|
||||
var isSettled = Model.Status == InvoiceStatus.Settled;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" @(env.IsDeveloping ? " data-devenv" : "")>
|
||||
<head>
|
||||
<partial name="LayoutHead" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 10000);
|
||||
</script>
|
||||
}
|
||||
<style>
|
||||
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="min-vh-100 d-flex flex-column">
|
||||
<main class="flex-grow-1 py-5">
|
||||
<div class="container" style="max-width:720px;">
|
||||
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) { { "Margin", "mb-4" } })"/>
|
||||
|
||||
<div class="d-flex flex-column justify-content-center gap-4">
|
||||
<h1 class="h3 text-center">@ViewData["Title"]</h1>
|
||||
<div id="InvoiceSummary" class="bg-tile p-3 p-sm-4 rounded d-flex flex-wrap align-items-center">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-processing">
|
||||
The invoice has detected a payment but is still waiting to be settled.
|
||||
</div>
|
||||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<div class="mx-auto">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
</div>
|
||||
}
|
||||
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<button type="button" class="btn btn-link p-0 d-print-none fw-semibold order-1" onclick="window.print()">
|
||||
Print
|
||||
</button>
|
||||
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
|
||||
</div>
|
||||
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@CurrencyNameTable.DisplayFormatCurrency(Model.Amount, Model.Currency)</dt>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<dd class="text-muted mb-0 fw-semibold">Date</dd>
|
||||
<dt class="fs-5 mb-0 text-nowrap fw-semibold">@Model.Timestamp.ToBrowserDate()</dt>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<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">@Model.OrderId</dt>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<small class="d-block text-muted text-center px-4">This page will refresh periodically until the invoice is settled.</small>
|
||||
}
|
||||
else if (isSettled)
|
||||
{
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
|
||||
<h2 class="h4 mb-3 d-print-none">Payment Details</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<table class="table my-0">
|
||||
<tr>
|
||||
<th class="fw-normal text-secondary">Destination</th>
|
||||
<th class="fw-normal text-secondary">Received</th>
|
||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||
<th class="fw-normal text-secondary text-end">Rate</th>
|
||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||
</tr>
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="text-break">
|
||||
<code>@payment.Destination</code>
|
||||
</td>
|
||||
<td>@payment.ReceivedDate.ToString("g")</td>
|
||||
<td class="text-end">@payment.PaidFormatted</td>
|
||||
<td class="text-end">@payment.RateFormatted</td>
|
||||
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
|
||||
</tr>
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="fw-normal" colspan="5">
|
||||
<span class="text-secondary">Transaction Id:</span>
|
||||
@if (!string.IsNullOrEmpty(payment.Link))
|
||||
{
|
||||
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-break">@payment.Id</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</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 d-print-none">Additional Data</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pt-2 pb-4 d-print-none">
|
||||
<p class="container text-center" permission="@Policies.CanViewInvoices">
|
||||
<a asp-action="Invoice" asp-route-invoiceId="@Model.InvoiceId">
|
||||
Admin details
|
||||
</a>
|
||||
</p>
|
||||
<div class="container d-flex flex-wrap align-items-center justify-content-center">
|
||||
<span class="text-muted mx-2">
|
||||
Powered by <a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">BTCPay Server</a>
|
||||
</span>
|
||||
@if (!Theme.CustomTheme)
|
||||
{
|
||||
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
|
||||
}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot"/>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
<table class="table table-hover table-responsive-md removetopborder">
|
||||
<table class="table table-hover my-0">
|
||||
@foreach (var (key, value) in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
|
@ -8,17 +8,17 @@
|
|||
{
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
<th class="w-150px">@key</th>
|
||||
<th class="w-150px">@Safe.Raw(key)</th>
|
||||
}
|
||||
<td>
|
||||
@if (str.StartsWith("http"))
|
||||
{
|
||||
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@value
|
||||
}
|
||||
@if (Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
<a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Safe.Raw(value?.ToString())
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (value is Dictionary<string, object>subItems)
|
||||
|
@ -26,22 +26,22 @@
|
|||
@* This is the array case *@
|
||||
if (subItems.Count == 1 && subItems.First().Value is string str2)
|
||||
{
|
||||
<th class="w-150px">@key</th>
|
||||
<th class="w-150px">@Safe.Raw(key)</th>
|
||||
<td>
|
||||
@if (str2.StartsWith("http"))
|
||||
@if (Uri.IsWellFormedUriString(str2, UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
<a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
|
||||
<a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@subItems.First().Value
|
||||
@Safe.Raw(subItems.First().Value?.ToString())
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td colspan="2">
|
||||
@Html.Raw($"<h{Model.Level + 3} class='mt-3'>{key}</h{Model.Level + 3}>")
|
||||
<td colspan="2" >
|
||||
@Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
|
||||
<partial name="PosData" model="(subItems, Model.Level + 1)"/>
|
||||
</td>
|
||||
}
|
||||
|
|
|
@ -238,128 +238,129 @@
|
|||
<div class="col">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
|
||||
<h2 class="h4 mb-0">Payment History</h2>
|
||||
<div class="table-responsive">
|
||||
<noscript>
|
||||
@if (Model.Invoices == null || !Model.Invoices.Any())
|
||||
<noscript>
|
||||
@if (Model.Invoices == null || !Model.Invoices.Any())
|
||||
{
|
||||
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="invoice table">
|
||||
<thead>
|
||||
<tr class="table-borderless">
|
||||
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
|
||||
<th class="fw-normal text-secondary w-175px">Expiry</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px"></th>
|
||||
<th class="fw-normal text-secondary text-end">Status</th>
|
||||
</tr>
|
||||
<tr class="table-borderless">
|
||||
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
|
||||
<th class="fw-normal text-secondary w-175px">Expiry</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px"></th>
|
||||
<th class="fw-normal text-secondary text-end">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="table-borderless table-light">
|
||||
<td>@invoice.Id</td>
|
||||
<td>@invoice.ExpiryDate.ToString("g")</td>
|
||||
<td class="text-end">@invoice.AmountFormatted</td>
|
||||
<td class="text-end"></td>
|
||||
<td class="text-end text-print-default">
|
||||
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (invoice.Payments != null && invoice.Payments.Any())
|
||||
{
|
||||
<tr class="table-borderless table-light">
|
||||
<td>@invoice.Id</td>
|
||||
<td>@invoice.ExpiryDate.ToString("g")</td>
|
||||
<td class="text-end">@invoice.AmountFormatted</td>
|
||||
<td class="text-end"></td>
|
||||
<td class="text-end text-print-default">
|
||||
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
|
||||
</td>
|
||||
<th class="fw-normal text-secondary">Destination</th>
|
||||
<th class="fw-normal text-secondary">Received</th>
|
||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||
<th class="fw-normal text-secondary text-end">Rate</th>
|
||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||
</tr>
|
||||
@if (invoice.Payments != null && invoice.Payments.Any())
|
||||
@foreach (var payment in invoice.Payments)
|
||||
{
|
||||
<tr class="table-borderless table-light">
|
||||
<th class="fw-normal text-secondary">Destination</th>
|
||||
<th class="fw-normal text-secondary">Received</th>
|
||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||
<th class="fw-normal text-secondary text-end">Rate</th>
|
||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||
<td class="text-break"><code>@payment.Destination</code></td>
|
||||
<td>@payment.ReceivedDate.ToString("g")</td>
|
||||
<td class="text-end">@payment.PaidFormatted</td>
|
||||
<td class="text-end">@payment.RateFormatted</td>
|
||||
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
|
||||
</tr>
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="fw-normal" colspan="5">
|
||||
<span class="text-secondary">Transaction Id:</span>
|
||||
@if (!string.IsNullOrEmpty(payment.Link))
|
||||
{
|
||||
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-break">@payment.Id</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@foreach (var payment in invoice.Payments)
|
||||
{
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="text-break"><code>@payment.Destination</code></td>
|
||||
<td>@payment.ReceivedDate.ToString("g")</td>
|
||||
<td class="text-end">@payment.PaidFormatted</td>
|
||||
<td class="text-end">@payment.RateFormatted</td>
|
||||
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
|
||||
</tr>
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="fw-normal" colspan="5">
|
||||
<span class="text-secondary">Transaction Id:</span>
|
||||
@if (!string.IsNullOrEmpty(payment.Link))
|
||||
{
|
||||
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-break">@payment.Id</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</noscript>
|
||||
|
||||
<template v-if="!srvModel.invoices || srvModel.invoices.length == 0">
|
||||
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
}
|
||||
</noscript>
|
||||
<template v-if="!srvModel.invoices || srvModel.invoices.length == 0">
|
||||
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="table-responsive">
|
||||
<table v-for="invoice of srvModel.invoices" :key="invoice.id" class="invoice table">
|
||||
<thead>
|
||||
<tr class="table-borderless">
|
||||
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
|
||||
<th class="fw-normal text-secondary w-175px">Expiry</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px"></th>
|
||||
<th class="fw-normal text-secondary text-end">Status</th>
|
||||
</tr>
|
||||
<tr class="table-borderless">
|
||||
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
|
||||
<th class="fw-normal text-secondary w-175px">Expiry</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
|
||||
<th class="fw-normal text-secondary text-end w-125px"></th>
|
||||
<th class="fw-normal text-secondary text-end">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="table-borderless table-light">
|
||||
<td>{{invoice.id}}</td>
|
||||
<td v-text="formatDate(invoice.expiryDate)"></td>
|
||||
<td class="text-end">{{invoice.amountFormatted}}</td>
|
||||
<td class="text-end"></td>
|
||||
<td class="text-end text-print-default">
|
||||
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="invoice.payments && invoice.payments.length > 0">
|
||||
<tr class="table-borderless table-light">
|
||||
<td>{{invoice.id}}</td>
|
||||
<td v-text="formatDate(invoice.expiryDate)"></td>
|
||||
<td class="text-end">{{invoice.amountFormatted}}</td>
|
||||
<td class="text-end"></td>
|
||||
<td class="text-end text-print-default">
|
||||
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
|
||||
</td>
|
||||
<th class="fw-normal text-secondary">Destination</th>
|
||||
<th class="fw-normal text-secondary">Received</th>
|
||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||
<th class="fw-normal text-secondary text-end">Rate</th>
|
||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||
</tr>
|
||||
<template v-if="invoice.payments && invoice.payments.length > 0">
|
||||
<template v-for="payment of invoice.payments">
|
||||
<tr class="table-borderless table-light">
|
||||
<th class="fw-normal text-secondary">Destination</th>
|
||||
<th class="fw-normal text-secondary">Received</th>
|
||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||
<th class="fw-normal text-secondary text-end">Rate</th>
|
||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||
<td class="text-break"><code>{{payment.destination}}</code></td>
|
||||
<td v-text="formatDate(payment.receivedDate)"></td>
|
||||
<td class="text-end">{{payment.paidFormatted}}</td>
|
||||
<td class="text-end">{{payment.rateFormatted}}</td>
|
||||
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
|
||||
</tr>
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="fw-normal" colspan="5">
|
||||
<span class="text-secondary">Transaction Id:</span>
|
||||
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
|
||||
<span v-else>{{payment.id}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="payment of invoice.payments">
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="text-break"><code>{{payment.destination}}</code></td>
|
||||
<td v-text="formatDate(payment.receivedDate)"></td>
|
||||
<td class="text-end">{{payment.paidFormatted}}</td>
|
||||
<td class="text-end">{{payment.rateFormatted}}</td>
|
||||
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
|
||||
</tr>
|
||||
<tr class="table-borderless table-light">
|
||||
<td class="fw-normal" colspan="5">
|
||||
<span class="text-secondary">Transaction Id:</span>
|
||||
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
|
||||
<span v-else>{{payment.id}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -55,6 +55,19 @@
|
|||
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
|
||||
</div>
|
||||
<h3 class="mb-3">Public receipt</h3>
|
||||
<div class="form-check my-1">
|
||||
<input asp-for="ReceiptOptions.Enabled" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ReceiptOptions.Enabled" class="form-check-label"></label>
|
||||
</div>
|
||||
<div class="form-check my-1">
|
||||
<input asp-for="ReceiptOptions.ShowPayments" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ReceiptOptions.ShowPayments" class="form-check-label"></label>
|
||||
</div>
|
||||
<div class="form-check my-1">
|
||||
<input asp-for="ReceiptOptions.ShowQR" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="ReceiptOptions.ShowQR" class="form-check-label"></label>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 class="mt-5 mb-3">Language</h3>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<table class="table table-hover removetopborder">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<td class="text-end">@Model.Label</td>
|
||||
|
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "በ Changelly ይክፈሉ",
|
||||
"Close": "ዝጋ",
|
||||
"NotPaid_ExtraTransaction": "ደረሰኙ ሙሉ በሙሉ አልተከፈለውም. እባክዎ የገንዘብ መጠን ለመሸፈን ሌላ ግብይት ይላኩ",
|
||||
"Recommended_Fee": "የሚመከር ክፍያ ፦ {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "የሚመከር ክፍያ ፦ {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "ادفع عن طريق خدمة Changelly",
|
||||
"Close": "اغلاق",
|
||||
"NotPaid_ExtraTransaction": "لم يتم دفع الفاتورة بشكل كامل. من فضلك ارسل تحويلة اخري تغطي الثمن المتبقي.",
|
||||
"Recommended_Fee": "العمولة المنصوح بها: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "العمولة المنصوح بها: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Changelly vasitəsilə ödə",
|
||||
"Close": "Qapat",
|
||||
"NotPaid_ExtraTransaction": "Faktura tam ödənilməyib. Lütfən məbləği tam ödəmək üçün daha bir tranzaksiya həyata keçirin.",
|
||||
"Recommended_Fee": "Tövsiyə edilən komissiya: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Tövsiyə edilən komissiya: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Плати с Changelly",
|
||||
"Close": "Затвори",
|
||||
"NotPaid_ExtraTransaction": "Фактурата не а платена изцяло. Моля пратете остатъка в допълнителна транзакция. ",
|
||||
"Recommended_Fee": "Препоръчена тарифа: {{feeRate}} сат/байт (sat/byte)"
|
||||
"Recommended_Fee": "Препоръчена тарифа: {{feeRate}} сат/байт (sat/byte)",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Plati sa Changelly",
|
||||
"Close": "Zatvori",
|
||||
"NotPaid_ExtraTransaction": "Račun nije plaćen u cijelosti. Pošaljite još jednu transakciju kako biste pokrili iznos koji dospijeva.",
|
||||
"Recommended_Fee": "Preporučena naknada: {{feeRate}} sat / bajt"
|
||||
"Recommended_Fee": "Preporučena naknada: {{feeRate}} sat / bajt",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pagueu amb Changelly",
|
||||
"Close": "Tancar",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Zaplatit přes Changelly",
|
||||
"Close": "Zavřít",
|
||||
"NotPaid_ExtraTransaction": "Faktura nebyla uhrazena v plné výši. Zašlete prosím další transakci na úhradu dlužné částky.",
|
||||
"Recommended_Fee": "Doporučený poplatek: {{feeRate}} sat/bajt"
|
||||
"Recommended_Fee": "Doporučený poplatek: {{feeRate}} sat/bajt",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Betal med Changelly",
|
||||
"Close": "Luk",
|
||||
"NotPaid_ExtraTransaction": "Fakturaen er ikke fuldt ud betalt. Venligst send endnu en transaktion for at dække for den manglende mængde.",
|
||||
"Recommended_Fee": "Anbefalt gebyr: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Anbefalt gebyr: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Zahlen mit Changelly",
|
||||
"Close": "Schließen",
|
||||
"NotPaid_ExtraTransaction": "Die Rechnung wurde nicht vollständig bezahlt. Bitte senden Sie den fehlenden Betrag, um die Rechnung zu begleichen.",
|
||||
"Recommended_Fee": "Empfohlene Netzwerk Gebührenrate: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Empfohlene Netzwerk Gebührenrate: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Πληρώστε με Changelly",
|
||||
"Close": "Κλείσιμο",
|
||||
"NotPaid_ExtraTransaction": "Το τιμολόγιο δεν έχει πληρωθεί πλήρως. Στείλτε άλλη συναλλαγή για να καλύψετε το οφειλόμενο ποσό.",
|
||||
"Recommended_Fee": "Προτεινόμενη χρέωση: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Προτεινόμενη χρέωση: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pagar con Changelly",
|
||||
"Close": "Cerrar",
|
||||
"NotPaid_ExtraTransaction": "La factura no ha sido pagada en su totalidad. Por favor envía otra transacción para cubrir el monto faltante.",
|
||||
"Recommended_Fee": "Comisión recomendada: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Comisión recomendada: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Maksa käyttäen Changelly:ä",
|
||||
"Close": "Sulje",
|
||||
"NotPaid_ExtraTransaction": "Laskua ei ole maksettu kokonaan. Lähetä uusi maksu erääntyvän summan kattamiseksi.",
|
||||
"Recommended_Fee": "Suositeltu siirtomaksu: {{feeRate}} sat/tavu"
|
||||
"Recommended_Fee": "Suositeltu siirtomaksu: {{feeRate}} sat/tavu",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Payer avec Changelly",
|
||||
"Close": "Fermer",
|
||||
"NotPaid_ExtraTransaction": "La facture n'as pas été réglée dans sa totalité. Veuillez envoyer une autre transaction pour la compléter.",
|
||||
"Recommended_Fee": "Frais recommandés: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Frais recommandés: {{feeRate}} sat/byte",
|
||||
"View receipt": "Voir le reçu"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "תשלום עם Changelly",
|
||||
"Close": "סגירה",
|
||||
"NotPaid_ExtraTransaction": "החשבונית לא שולמה במלואה. נא לשלוח תשלום נוסף לכיסוי סכום החוב.",
|
||||
"Recommended_Fee": "עמלה מומלצת: {{feeRate}} סאט/בית"
|
||||
"Recommended_Fee": "עמלה מומלצת: {{feeRate}} סאט/בית",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Changelly द्वारा भुगतान करें",
|
||||
"Close": "बंद करे",
|
||||
"NotPaid_ExtraTransaction": "चालान का पूरा भुगतान नहीं किया गया है। कृपया चालान राशि पूरी करें।",
|
||||
"Recommended_Fee": "अनुशंसित शुल्क: {{शुल्क दर}} सैट/बाइट "
|
||||
"Recommended_Fee": "अनुशंसित शुल्क: {{शुल्क दर}} सैट/बाइट ",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Fizess Changelly-vel",
|
||||
"Close": "Bezár",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Վճարել Changelly-ով",
|
||||
"Close": "Փակել",
|
||||
"NotPaid_ExtraTransaction": "Հաշիվը ամբողջությամբ չի վճարվել: Խնդրում ենք ուղարկել մեկ այլ գործարք՝ մնացած գումարը վճարելու համար:",
|
||||
"Recommended_Fee": "Առաջարկվող վճար: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Առաջարկվող վճար: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Bayar dengan Changelly",
|
||||
"Close": "Tutup",
|
||||
"NotPaid_ExtraTransaction": "Tagihan ini belum dibayarkan sepenuhnya. Dimohon untuk membayarkan sisa pembayaran anda.",
|
||||
"Recommended_Fee": "Biaya yang disarankan: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Biaya yang disarankan: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Borga með Changelly",
|
||||
"Close": "Loka",
|
||||
"NotPaid_ExtraTransaction": "Reikningurinn hefur ekki verið greiddur að fullu. Vinsamlegast greiddu restina.",
|
||||
"Recommended_Fee": "Ráðlögð þóknun: {{feeRate}} sat/bæti"
|
||||
"Recommended_Fee": "Ráðlögð þóknun: {{feeRate}} sat/bæti",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Paga con Changelly",
|
||||
"Close": "Chiudi",
|
||||
"NotPaid_ExtraTransaction": "La fattura non è stata pagata per intero. Per favore effettua una nuova transazione per coprire l'importo dovuto.",
|
||||
"Recommended_Fee": "Fee raccomandate: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Fee raccomandate: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Changellyでのお支払い",
|
||||
"Close": "閉じる",
|
||||
"NotPaid_ExtraTransaction": "請求金額の全額が支払われていません。未払い分の別のトランザクションをお送りください。",
|
||||
"Recommended_Fee": "推奨手数料: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "推奨手数料: {{feeRate}} sat/byte",
|
||||
"View receipt": "領収書へ"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Changelly를 이용해 지불하기",
|
||||
"Close": "닫기",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "추천하는 수수료: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "추천하는 수수료: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Maksāt ar Changelly",
|
||||
"Close": "Aizvērt",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Betalen met Changelly",
|
||||
"Close": "Sluiten",
|
||||
"NotPaid_ExtraTransaction": "Het factuur is niet volledig betaald. Stuur nog een transactie om het resterende bedrag te betalen.",
|
||||
"Recommended_Fee": "Aanbevolen vergoeding: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Aanbevolen vergoeding: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Betal med Changelly",
|
||||
"Close": "Lukk",
|
||||
"NotPaid_ExtraTransaction": "Denne fakturaen har ikke blitt betalt fullt ut. Send en ny transaksjon med resten av beløpet.",
|
||||
"Recommended_Fee": "Anbefalt avgift: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Anbefalt avgift: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Zapłać z Changelly",
|
||||
"Close": "Zamknij",
|
||||
"NotPaid_ExtraTransaction": "Faktura nie została w pełni opłacona. Proszę wyślij dodatkową transakcje z resztą brakujących środków.",
|
||||
"Recommended_Fee": "Zalecana opłata transakcyjna: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Zalecana opłata transakcyjna: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pagar com Changelly",
|
||||
"Close": "Fechar",
|
||||
"NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Envie outra transação para cobrir o valor devido.",
|
||||
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pagar com Changelly",
|
||||
"Close": "Fechar",
|
||||
"NotPaid_ExtraTransaction": "A fatura não foi paga na totalidade. Por favor, faça outra transação para cobrir o valor em falta.",
|
||||
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Platiți cu Changelly",
|
||||
"Close": "Închideți",
|
||||
"NotPaid_ExtraTransaction": "Factura nu a fost achitată integral. Vă rugăm să trimiteți o altă plată pentru a acoperi suma datorată.",
|
||||
"Recommended_Fee": "Taxa recomandată: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Taxa recomandată: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Оплатить с помощью Changelly",
|
||||
"Close": "Закрыть",
|
||||
"NotPaid_ExtraTransaction": "Счет не был оплачен полностью. Пожалуйста, отправьте еще одну транзакцию для покрытия суммы задолженности.",
|
||||
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт"
|
||||
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Zaplatiť cez Changelly",
|
||||
"Close": "Zatvoriť",
|
||||
"NotPaid_ExtraTransaction": "Faktúra nebola uhradená v plnej výške. Prosím pošlite inú transakciu na pokrytie dlžnej sumy.",
|
||||
"Recommended_Fee": "Odporúčaný poplatok: {{feeRate}} sat/bajt"
|
||||
"Recommended_Fee": "Odporúčaný poplatok: {{feeRate}} sat/bajt",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Plačilo z Changelly",
|
||||
"Close": "Zapri",
|
||||
"NotPaid_ExtraTransaction": "Račun ni bil plačan v celoti. Potrebna je nova transakcija za doplačilo.",
|
||||
"Recommended_Fee": "Priporočena provizija: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Priporočena provizija: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Plati putem Changelly-ja",
|
||||
"Close": "Zatvori",
|
||||
"NotPaid_ExtraTransaction": "Račun nije plaćen u potpunosti. Pošalji još jednu transakciju da izmiriš Dug.",
|
||||
"Recommended_Fee": "Preporučena provizija: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Preporučena provizija: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Betala med Changelly",
|
||||
"Close": "Stäng",
|
||||
"NotPaid_ExtraTransaction": "Fakturabetalningen är ofullständig. Vänligen betala kvarstående belopp.",
|
||||
"Recommended_Fee": "Rekommenderad avgift: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Rekommenderad avgift: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Changelly ile Öde",
|
||||
"Close": "Kapat",
|
||||
"NotPaid_ExtraTransaction": "Fatura tam olarak ödenmedi. Lütfen ödenmesi gereken tutarı karşılamak için başka bir işlem gönderin.",
|
||||
"Recommended_Fee": "Önerilen ücret: {{feeRate}} sat/bayt"
|
||||
"Recommended_Fee": "Önerilen ücret: {{feeRate}} sat/bayt",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Оплатити за допомогою Changelly",
|
||||
"Close": "Закрити",
|
||||
"NotPaid_ExtraTransaction": "Рахунок не був оплачений повністю. Будь ласка, надішліть ще одну транзакцію для покриття суми заборгованості.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Pay with Changelly",
|
||||
"Close": "Close",
|
||||
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "通过Changelly支付",
|
||||
"Close": "关闭",
|
||||
"NotPaid_ExtraTransaction": "发票还没有付清。请订单到期之前发送另一笔交易完成支付。",
|
||||
"Recommended_Fee": "推荐费率::{{feeRate}} sat/byte"
|
||||
"Recommended_Fee": "推荐费率::{{feeRate}} sat/byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -48,5 +48,6 @@
|
|||
"Pay with Changelly": "Khokha nge-Changelly",
|
||||
"Close": "Vala",
|
||||
"NotPaid_ExtraTransaction": "I-invoice ayikakhokhelwa ngokugcwele. Sicela uthumele enye imali oyokhokha ukuze ukhokhe inani elifanele.",
|
||||
"Recommended_Fee": "Imali enconyiwe: {{feeRate}} sat / byte"
|
||||
"Recommended_Fee": "Imali enconyiwe: {{feeRate}} sat / byte",
|
||||
"View receipt": "View receipt"
|
||||
}
|
|
@ -714,6 +714,11 @@
|
|||
"nullable": true,
|
||||
"$ref": "#/components/schemas/CheckoutOptions",
|
||||
"description": "Additional settings to customize the checkout flow"
|
||||
},
|
||||
"receipt": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/ReceiptOptions",
|
||||
"description": "Additional settings to customize the public receipt"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1061,6 +1066,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ReceiptOptions": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "A public page will be accessible once the invoice is settled. If null or unspecified, it will fallback to the store's settings. (The default store settings is true)"
|
||||
},
|
||||
"showQR": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Show the QR code of the receipt in the public receipt page. If null or unspecified, it will fallback to the store's settings. (The default store setting is true)"
|
||||
},
|
||||
"showPayments": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Show the payment list in the public receipt page. If null or unspecified, it will fallback to the store's settings. (The default store setting is true)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SpeedPolicy": {
|
||||
"type": "string",
|
||||
"description": "`\"HighSpeed\"`: 0 confirmations (1 confirmation if RBF enabled in transaction) \n`\"MediumSpeed\"`: 1 confirmation \n`\"LowMediumSpeed\"`: 2 confirmations \n`\"LowSpeed\"`: 6 confirmations\n",
|
||||
|
|
|
@ -329,6 +329,11 @@
|
|||
"default": false,
|
||||
"description": "If true, the checkout page will ask to enter an email address before accessing payment information."
|
||||
},
|
||||
"receipt": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/ReceiptOptions",
|
||||
"description": "Additional settings to customize the public receipt"
|
||||
},
|
||||
"lightningAmountInSatoshi": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
|
Loading…
Add table
Reference in a new issue