mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Implement topup invoices (#2730)
This commit is contained in:
@@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
public class CreateInvoiceRequest : InvoiceDataBase
|
public class CreateInvoiceRequest : InvoiceDataBase
|
||||||
{
|
{
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal? Amount { get; set; }
|
||||||
public string[] AdditionalSearchTerms { get; set; }
|
public string[] AdditionalSearchTerms { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
|
public enum InvoiceType
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
TopUp
|
||||||
|
}
|
||||||
public class InvoiceDataBase
|
public class InvoiceDataBase
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public decimal Amount { get; set; }
|
public InvoiceType Type { get; set; }
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
public JObject Metadata { get; set; }
|
public JObject Metadata { get; set; }
|
||||||
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
|
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
|
||||||
@@ -41,6 +46,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
public string CheckoutLink { get; set; }
|
public string CheckoutLink { get; set; }
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public InvoiceStatus Status { get; set; }
|
public InvoiceStatus Status { get; set; }
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ namespace BTCPayServer.Services.Rates
|
|||||||
}
|
}
|
||||||
|
|
||||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||||
|
|
||||||
public string FormatCurrency(string price, string currency)
|
public string FormatCurrency(string price, string currency)
|
||||||
{
|
{
|
||||||
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
|
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
|
||||||
@@ -110,9 +109,8 @@ namespace BTCPayServer.Services.Rates
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The value</param>
|
/// <param name="value">The value</param>
|
||||||
/// <param name="currency">Currency code</param>
|
/// <param name="currency">Currency code</param>
|
||||||
/// <param name="threeLetterSuffix">Add three letter suffix (like USD)</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string DisplayFormatCurrency(decimal value, string currency, bool threeLetterSuffix = true)
|
public string DisplayFormatCurrency(decimal value, string currency)
|
||||||
{
|
{
|
||||||
var provider = GetNumberFormatInfo(currency, true);
|
var provider = GetNumberFormatInfo(currency, true);
|
||||||
var currencyData = GetCurrencyData(currency, true);
|
var currencyData = GetCurrencyData(currency, true);
|
||||||
|
|||||||
@@ -226,6 +226,57 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
public async Task CanUsePayjoinForTopUp()
|
||||||
|
{
|
||||||
|
using (var s = SeleniumTester.Create())
|
||||||
|
{
|
||||||
|
await s.StartAsync();
|
||||||
|
s.RegisterNewUser(true);
|
||||||
|
var receiver = s.CreateNewStore();
|
||||||
|
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
|
||||||
|
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
|
||||||
|
|
||||||
|
var sender = s.CreateNewStore();
|
||||||
|
var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
|
||||||
|
var senderWalletId = new WalletId(sender.storeId, "BTC");
|
||||||
|
|
||||||
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||||
|
await s.FundStoreWallet(senderWalletId);
|
||||||
|
await s.FundStoreWallet(receiverWalletId);
|
||||||
|
|
||||||
|
var invoiceId = s.CreateInvoice(receiver.storeName, null, "BTC");
|
||||||
|
s.GoToInvoiceCheckout(invoiceId);
|
||||||
|
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||||
|
.GetAttribute("href");
|
||||||
|
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||||
|
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
|
||||||
|
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||||
|
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||||
|
s.Driver.SwitchTo().Alert().Accept();
|
||||||
|
s.Driver.FindElement(By.Id("Outputs_0__Amount")).Clear();
|
||||||
|
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys("0.023");
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||||
|
|
||||||
|
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||||
|
{
|
||||||
|
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||||
|
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||||
|
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
|
||||||
|
Assert.Equal(0.023m, invoice.Price);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Selenium", "Selenium")]
|
[Trait("Selenium", "Selenium")]
|
||||||
public async Task CanUsePayjoinViaUI()
|
public async Task CanUsePayjoinViaUI()
|
||||||
|
|||||||
@@ -331,11 +331,12 @@ namespace BTCPayServer.Tests
|
|||||||
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "/login"));
|
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "/login"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
|
public string CreateInvoice(string storeName, decimal? amount = 100, string currency = "USD", string refundEmail = "")
|
||||||
{
|
{
|
||||||
GoToInvoices();
|
GoToInvoices();
|
||||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||||
Driver.FindElement(By.Id("Amount")).SendKeys(amount.ToString(CultureInfo.InvariantCulture));
|
if (amount is decimal v)
|
||||||
|
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
|
||||||
var currencyEl = Driver.FindElement(By.Id("Currency"));
|
var currencyEl = Driver.FindElement(By.Id("Currency"));
|
||||||
currencyEl.Clear();
|
currencyEl.Clear();
|
||||||
currencyEl.SendKeys(currency);
|
currencyEl.SendKeys(currency);
|
||||||
|
|||||||
@@ -935,7 +935,8 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
|
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
|
||||||
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
|
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
|
||||||
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
|
(1000.0001m, "₹ 1,000.00 (INR)", "INR"),
|
||||||
|
(0.0m, "$0.00 (USD)", "USD")
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
|
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
|
||||||
@@ -2099,6 +2100,90 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanCreateTopupInvoices()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var rng = new Random();
|
||||||
|
var seed = rng.Next();
|
||||||
|
rng = new Random(seed);
|
||||||
|
Logs.Tester.LogInformation("Seed: " + seed);
|
||||||
|
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
|
||||||
|
{
|
||||||
|
await user.SetNetworkFeeMode(networkFeeMode);
|
||||||
|
await AssertTopUpBtcPrice(tester, user, Money.Coins(1.0m), 5000.0m, networkFeeMode);
|
||||||
|
await AssertTopUpBtcPrice(tester, user, Money.Coins(1.23456789m), 5000.0m * 1.23456789m, networkFeeMode);
|
||||||
|
// Check if there is no strange roundup issues
|
||||||
|
var v = (decimal)(rng.NextDouble() + 1.0);
|
||||||
|
v = Money.Coins(v).ToDecimal(MoneyUnit.BTC);
|
||||||
|
await AssertTopUpBtcPrice(tester, user, Money.Coins(v), 5000.0m * v, networkFeeMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
|
||||||
|
{
|
||||||
|
var cashCow = tester.ExplorerNode;
|
||||||
|
// First we try payment with a merchant having only BTC
|
||||||
|
var client = await user.CreateClient();
|
||||||
|
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||||
|
{
|
||||||
|
Amount = null,
|
||||||
|
Currency = "USD"
|
||||||
|
});
|
||||||
|
Assert.Equal(0m, invoice.Amount);
|
||||||
|
Assert.Equal(InvoiceType.TopUp, invoice.Type);
|
||||||
|
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
|
||||||
|
var paid = btcSent;
|
||||||
|
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network);
|
||||||
|
|
||||||
|
|
||||||
|
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||||
|
var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
|
||||||
|
.GetPaymentMethods()[btc]
|
||||||
|
.GetPaymentMethodDetails()
|
||||||
|
.AssertType<BitcoinLikeOnChainPaymentMethod>()
|
||||||
|
.GetNextNetworkFee();
|
||||||
|
if (networkFeeMode != NetworkFeeMode.Always)
|
||||||
|
{
|
||||||
|
networkFee = 0.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
cashCow.SendToAddress(invoiceAddress, paid);
|
||||||
|
|
||||||
|
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bitpayinvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
|
||||||
|
Assert.NotEqual(0.0m, bitpayinvoice.Price);
|
||||||
|
var due = Money.Parse(bitpayinvoice.CryptoInfo[0].CryptoPaid);
|
||||||
|
Assert.Equal(paid, due);
|
||||||
|
Assert.Equal(expectedPriceWithoutNetworkFee - networkFee * bitpayinvoice.Rate, bitpayinvoice.Price);
|
||||||
|
Assert.Equal(Money.Zero, bitpayinvoice.BtcDue);
|
||||||
|
Assert.Equal("paid", bitpayinvoice.Status);
|
||||||
|
Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString());
|
||||||
|
|
||||||
|
// Check if we index by price correctly once we know it
|
||||||
|
var invoices = await client.GetInvoices(user.StoreId, textSearch: $"{bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
Assert.Contains(invoices, inv => inv.Id == bitpayinvoice.Id);
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException)
|
||||||
|
{
|
||||||
|
Assert.False(true, "The bitpay's amount is not set");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanModifyRates()
|
public async Task CanModifyRates()
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
MonitoringExpiration = entity.MonitoringExpiration,
|
MonitoringExpiration = entity.MonitoringExpiration,
|
||||||
CreatedTime = entity.InvoiceTime,
|
CreatedTime = entity.InvoiceTime,
|
||||||
Amount = entity.Price,
|
Amount = entity.Price,
|
||||||
|
Type = entity.Type,
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
CheckoutLink = _linkGenerator.CheckoutLink(entity.Id, Request.Scheme, Request.Host, Request.PathBase),
|
CheckoutLink = _linkGenerator.CheckoutLink(entity.Id, Request.Scheme, Request.Host, Request.PathBase),
|
||||||
Status = entity.Status.ToModernStatus(),
|
Status = entity.Status.ToModernStatus(),
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ namespace BTCPayServer.Controllers
|
|||||||
cdCurrency.Divisibility);
|
cdCurrency.Divisibility);
|
||||||
model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility);
|
model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility);
|
||||||
model.RateThenText =
|
model.RateThenText =
|
||||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode,
|
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
|
||||||
true);
|
|
||||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||||
rateResult = await _RateProvider.FetchRate(
|
rateResult = await _RateProvider.FetchRate(
|
||||||
new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
|
new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
|
||||||
@@ -263,10 +262,9 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||||
model.CurrentRateText =
|
model.CurrentRateText =
|
||||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode,
|
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
|
||||||
true);
|
|
||||||
model.FiatAmount = paidCurrency;
|
model.FiatAmount = paidCurrency;
|
||||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency, true);
|
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency);
|
||||||
return View(model);
|
return View(model);
|
||||||
case RefundSteps.SelectRate:
|
case RefundSteps.SelectRate:
|
||||||
createPullPayment = new HostedServices.CreatePullPayment();
|
createPullPayment = new HostedServices.CreatePullPayment();
|
||||||
@@ -562,6 +560,7 @@ namespace BTCPayServer.Controllers
|
|||||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||||
InvoiceCurrency = invoice.Currency,
|
InvoiceCurrency = invoice.Currency,
|
||||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||||
|
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
|
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
|
||||||
CustomerEmail = invoice.RefundMail,
|
CustomerEmail = invoice.RefundMail,
|
||||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||||
@@ -846,17 +845,12 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
|
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
if (model.Amount is null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(model.Amount), "Thhe invoice amount can't be empty");
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||||
{
|
{
|
||||||
Price = model.Amount.Value,
|
Price = model.Amount,
|
||||||
Currency = model.Currency,
|
Currency = model.Currency,
|
||||||
PosData = model.PosData,
|
PosData = model.PosData,
|
||||||
OrderId = model.OrderId,
|
OrderId = model.OrderId,
|
||||||
|
|||||||
@@ -119,7 +119,16 @@ namespace BTCPayServer.Controllers
|
|||||||
entity.Metadata.Physical = invoice.Physical;
|
entity.Metadata.Physical = invoice.Physical;
|
||||||
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
|
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
|
||||||
entity.Currency = invoice.Currency;
|
entity.Currency = invoice.Currency;
|
||||||
entity.Price = price;
|
if (price is decimal vv)
|
||||||
|
{
|
||||||
|
entity.Price = vv;
|
||||||
|
entity.Type = InvoiceType.Standard;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.Price = 0m;
|
||||||
|
entity.Type = InvoiceType.TopUp;
|
||||||
|
}
|
||||||
|
|
||||||
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
||||||
entity.RedirectAutomatically =
|
entity.RedirectAutomatically =
|
||||||
@@ -161,7 +170,16 @@ namespace BTCPayServer.Controllers
|
|||||||
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||||
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
|
invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD";
|
||||||
entity.Currency = invoice.Currency;
|
entity.Currency = invoice.Currency;
|
||||||
entity.Price = invoice.Amount;
|
if (invoice.Amount is decimal v)
|
||||||
|
{
|
||||||
|
entity.Price = v;
|
||||||
|
entity.Type = InvoiceType.Standard;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.Price = 0.0m;
|
||||||
|
entity.Type = InvoiceType.TopUp;
|
||||||
|
}
|
||||||
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
||||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model == null || model.Price <= 0)
|
if (model == null || (model.Price is decimal v ? v <= 0 : false))
|
||||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
|
|||||||
@@ -999,7 +999,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||||
var model = new PayButtonViewModel
|
var model = new PayButtonViewModel
|
||||||
{
|
{
|
||||||
Price = 10,
|
Price = null,
|
||||||
Currency = DEFAULT_CURRENCY,
|
Currency = DEFAULT_CURRENCY,
|
||||||
ButtonSize = 2,
|
ButtonSize = 2,
|
||||||
UrlRoot = appUrl,
|
UrlRoot = appUrl,
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ namespace BTCPayServer.HostedServices
|
|||||||
|
|
||||||
public bool Dirty => _Dirty;
|
public bool Dirty => _Dirty;
|
||||||
public bool Unaffect => _Unaffect;
|
public bool Unaffect => _Unaffect;
|
||||||
|
|
||||||
|
bool _IsBlobUpdated;
|
||||||
|
public bool IsBlobUpdated => _IsBlobUpdated;
|
||||||
|
public void BlobUpdated()
|
||||||
|
{
|
||||||
|
_IsBlobUpdated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly InvoiceRepository _InvoiceRepository;
|
readonly InvoiceRepository _InvoiceRepository;
|
||||||
@@ -83,13 +90,26 @@ namespace BTCPayServer.HostedServices
|
|||||||
return;
|
return;
|
||||||
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
||||||
{
|
{
|
||||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
var isPaid = invoice.IsUnsetTopUp() ?
|
||||||
|
accounting.Paid > Money.Zero :
|
||||||
|
accounting.Paid >= accounting.MinimumTotalDue;
|
||||||
|
if (isPaid)
|
||||||
{
|
{
|
||||||
if (invoice.Status == InvoiceStatusLegacy.New)
|
if (invoice.Status == InvoiceStatusLegacy.New)
|
||||||
{
|
{
|
||||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
||||||
invoice.Status = InvoiceStatusLegacy.Paid;
|
invoice.Status = InvoiceStatusLegacy.Paid;
|
||||||
|
if (invoice.IsUnsetTopUp())
|
||||||
|
{
|
||||||
|
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||||
|
invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate;
|
||||||
|
accounting = paymentMethod.Calculate();
|
||||||
|
context.BlobUpdated();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||||
|
}
|
||||||
context.UnaffectAddresses();
|
context.UnaffectAddresses();
|
||||||
context.MarkDirty();
|
context.MarkDirty();
|
||||||
}
|
}
|
||||||
@@ -293,6 +313,10 @@ namespace BTCPayServer.HostedServices
|
|||||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||||
}
|
}
|
||||||
|
if (updateContext.IsBlobUpdated)
|
||||||
|
{
|
||||||
|
await _InvoiceRepository.UpdateInvoicePrice(invoice.Id, invoice);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var evt in updateContext.Events)
|
foreach (var evt in updateContext.Events)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace BTCPayServer.ModelBinders
|
|||||||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
|
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
|
||||||
model = null;
|
model = null;
|
||||||
}
|
}
|
||||||
else if (type == typeof(decimal))
|
else if (type == typeof(decimal) || type == typeof(decimal?))
|
||||||
{
|
{
|
||||||
model = decimal.Parse(value, _supportedStyles, culture);
|
model = decimal.Parse(value, _supportedStyles, culture);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace BTCPayServer.Models
|
|||||||
[JsonProperty(PropertyName = "currency", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(PropertyName = "currency", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
[JsonProperty(PropertyName = "price", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(PropertyName = "price", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public decimal Price { get; set; }
|
public decimal? Price { get; set; }
|
||||||
[JsonProperty(PropertyName = "notificationEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(PropertyName = "notificationEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public string NotificationEmail { get; set; }
|
public string NotificationEmail { get; set; }
|
||||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
Currency = "USD";
|
Currency = "USD";
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
|
||||||
public decimal? Amount
|
public decimal? Amount
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
public string DefaultLang { get; set; }
|
public string DefaultLang { get; set; }
|
||||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||||
public bool IsModal { get; set; }
|
public bool IsModal { get; set; }
|
||||||
|
public bool IsUnsetTopUp { get; set; }
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
public string BtcAddress { get; set; }
|
public string BtcAddress { get; set; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
public class PayButtonViewModel
|
public class PayButtonViewModel
|
||||||
{
|
{
|
||||||
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
|
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
|
||||||
public decimal Price { get; set; }
|
public decimal? Price { get; set; }
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
if (additionalFee > Money.Zero)
|
if (additionalFee > Money.Zero)
|
||||||
{
|
{
|
||||||
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
|
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
|
||||||
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
|
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero && !invoice.IsUnsetTopUp(); i++)
|
||||||
{
|
{
|
||||||
if (disableoutputsubstitution)
|
if (disableoutputsubstitution)
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using NBitcoin.DataEncoders;
|
|||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
@@ -396,6 +397,10 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
public double PaymentTolerance { get; set; }
|
public double PaymentTolerance { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
public InvoiceType Type { get; set; }
|
||||||
|
|
||||||
public bool IsExpired()
|
public bool IsExpired()
|
||||||
{
|
{
|
||||||
return DateTimeOffset.UtcNow > ExpirationTime;
|
return DateTimeOffset.UtcNow > ExpirationTime;
|
||||||
@@ -681,6 +686,11 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
throw new InvalidOperationException("Not a legacy invoice");
|
throw new InvalidOperationException("Not a legacy invoice");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsUnsetTopUp()
|
||||||
|
{
|
||||||
|
return Type == InvoiceType.TopUp && Price == 0.0m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum InvoiceStatusLegacy
|
public enum InvoiceStatusLegacy
|
||||||
@@ -865,6 +875,10 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Money NetworkFee { get; set; }
|
public Money NetworkFee { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Total amount of network fee to pay to the invoice
|
||||||
|
/// </summary>
|
||||||
|
public Money NetworkFeeAlreadyPaid { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Minimum required to be paid in order to accept invoice as paid
|
/// Minimum required to be paid in order to accept invoice as paid
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Money MinimumTotalDue { get; set; }
|
public Money MinimumTotalDue { get; set; }
|
||||||
@@ -991,13 +1005,14 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||||
int txRequired = 0;
|
int txRequired = 0;
|
||||||
|
decimal networkFeeAlreadyPaid = 0.0m;
|
||||||
_ = ParentEntity.GetPayments(true)
|
_ = ParentEntity.GetPayments(true)
|
||||||
.Where(p => paymentPredicate(p))
|
.Where(p => paymentPredicate(p))
|
||||||
.OrderBy(p => p.ReceivedTime)
|
.OrderBy(p => p.ReceivedTime)
|
||||||
.Select(_ =>
|
.Select(_ =>
|
||||||
{
|
{
|
||||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
|
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
|
||||||
|
networkFeeAlreadyPaid += txFee;
|
||||||
paid += _.GetValue(paymentMethods, GetId(), null, precision);
|
paid += _.GetValue(paymentMethods, GetId(), null, precision);
|
||||||
if (!paidEnough)
|
if (!paidEnough)
|
||||||
{
|
{
|
||||||
@@ -1029,6 +1044,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||||
|
accounting.NetworkFeeAlreadyPaid = Money.Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision));
|
||||||
// If the total due is 0, there is no payment tolerance to calculate
|
// If the total due is 0, there is no payment tolerance to calculate
|
||||||
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0
|
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0
|
||||||
? 0
|
? 0
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
|
|
||||||
textSearch.Add(invoice.Id);
|
textSearch.Add(invoice.Id);
|
||||||
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
|
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
|
||||||
|
if (!invoice.IsUnsetTopUp())
|
||||||
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
|
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
|
||||||
textSearch.Add(invoice.Metadata.OrderId);
|
textSearch.Add(invoice.Metadata.OrderId);
|
||||||
textSearch.Add(invoice.StoreId);
|
textSearch.Add(invoice.StoreId);
|
||||||
@@ -425,6 +426,22 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice)
|
||||||
|
{
|
||||||
|
if (invoice.Type != InvoiceType.TopUp)
|
||||||
|
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice));
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||||
|
if (invoiceData == null)
|
||||||
|
return;
|
||||||
|
var blob = invoiceData.GetBlob(_Networks);
|
||||||
|
blob.Price = invoice.Price;
|
||||||
|
AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) });
|
||||||
|
invoiceData.Blob = ToBytes(blob, null);
|
||||||
|
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task MassArchive(string[] invoiceIds)
|
public async Task MassArchive(string[] invoiceIds)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,10 +95,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-item-order__right">
|
<div class="single-item-order__right">
|
||||||
<div class="single-item-order__right__btc-price" v-if="srvModel.status === 'paid'">
|
<div class="single-item-order__right__btc-price" v-if="srvModel.status === 'paid' && !srvModel.isUnsetTopUp">
|
||||||
<span>{{ srvModel.btcPaid }} {{ srvModel.cryptoCode }}</span>
|
<span>{{ srvModel.btcPaid }} {{ srvModel.cryptoCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-item-order__right__btc-price" v-else>
|
<div class="single-item-order__right__btc-price" v-if="srvModel.status !== 'paid' && !srvModel.isUnsetTopUp">
|
||||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat && srvModel.cryptoCode">
|
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat && srvModel.cryptoCode">
|
||||||
@@ -106,10 +106,10 @@
|
|||||||
<span v-else>1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}</span>
|
<span v-else>1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="fa fa-angle-double-down"></span>
|
<span class="fa fa-angle-double-down" v-if="!srvModel.isUnsetTopUp"></span>
|
||||||
<span class="fa fa-angle-double-up"></span>
|
<span class="fa fa-angle-double-up" v-if="!srvModel.isUnsetTopUp"></span>
|
||||||
</div>
|
</div>
|
||||||
<line-items>
|
<line-items v-if="!srvModel.isUnsetTopUp">
|
||||||
<div class="extraPayment" v-if="srvModel.status === 'new' && srvModel.txCount > 1">
|
<div class="extraPayment" v-if="srvModel.status === 'new' && srvModel.txCount > 1">
|
||||||
{{$t("NotPaid_ExtraTransaction")}}
|
{{$t("NotPaid_ExtraTransaction")}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model BTCPayServer.Models.InvoicingModels.CreateInvoiceModel
|
@model BTCPayServer.Models.InvoicingModels.CreateInvoiceModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePageAndTitle(InvoiceNavPages.Create, "Create an invoice");
|
ViewData.SetActivePageAndTitle(InvoiceNavPages.Create, "Create an invoice");
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<form asp-action="CreateInvoice" method="post" id="create-invoice-form">
|
<form asp-action="CreateInvoice" method="post" id="create-invoice-form">
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Amount" class="form-label" data-required></label>
|
<label asp-for="Amount" class="form-label"></label>
|
||||||
<input asp-for="Amount" class="form-control" required />
|
<input asp-for="Amount" class="form-control" />
|
||||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bp-view payment manual-flow" id="copy" v-bind:class="{ 'active': currentTab == 'copy'}">
|
<div class="bp-view payment manual-flow" id="copy" v-bind:class="{ 'active': currentTab == 'copy'}">
|
||||||
<div class="manual__step-two__instructions">
|
<div class="manual__step-two__instructions" v-if="!srvModel.isUnsetTopUp">
|
||||||
<span v-html="$t('CompletePay_Body', srvModel)"></span>
|
<span v-html="$t('CompletePay_Body', srvModel)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyLabelPopup">
|
<div class="copyLabelPopup">
|
||||||
<span>{{$t("Copied")}}</span>
|
<span>{{$t("Copied")}}</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="copyBox">
|
<nav class="copyBox">
|
||||||
<div class="copySectionBox bottomBorder">
|
<div class="copySectionBox bottomBorder" v-if="!srvModel.isUnsetTopUp">
|
||||||
<label>{{$t("Amount")}}</label>
|
<label>{{$t("Amount")}}</label>
|
||||||
<div class="copyAmountText copy-cursor _copySpan">
|
<div class="copyAmountText copy-cursor _copySpan">
|
||||||
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
|
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
|
||||||
|
|||||||
@@ -58,9 +58,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-8">
|
<div class="form-group col-md-8">
|
||||||
<label class="form-label">Price</label>
|
<label class="form-label">Price</label>
|
||||||
<input name="price" type="text" class="form-control"
|
<input name="price" type="text" class="form-control" placeholder="(optional)"
|
||||||
v-model="srvModel.price" v-on:change="inputChanges"
|
v-model="srvModel.price" v-on:change="inputChanges"
|
||||||
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
|
v-validate="'decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
|
||||||
<small class="text-danger">{{ errors.first('price') }}</small>
|
<small class="text-danger">{{ errors.first('price') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
|
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ function inputChanges(event, buttonSize) {
|
|||||||
|
|
||||||
// Fixed amount: Add price and currency as hidden inputs
|
// Fixed amount: Add price and currency as hidden inputs
|
||||||
if (isFixedAmount) {
|
if (isFixedAmount) {
|
||||||
|
if (srvModel.price !== '')
|
||||||
html += addInput(priceInputName, srvModel.price);
|
html += addInput(priceInputName, srvModel.price);
|
||||||
if(allowCurrencySelection){
|
if(allowCurrencySelection){
|
||||||
html += addInput("currency", srvModel.currency);
|
html += addInput("currency", srvModel.currency);
|
||||||
|
|||||||
@@ -747,11 +747,6 @@
|
|||||||
},
|
},
|
||||||
"InvoiceDataBase": {
|
"InvoiceDataBase": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"amount": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "The amount of the invoice"
|
|
||||||
},
|
|
||||||
"currency": {
|
"currency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@@ -788,6 +783,15 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The store identifier that the invoice belongs to"
|
"description": "The store identifier that the invoice belongs to"
|
||||||
},
|
},
|
||||||
|
"amount": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"description": "The amount of the invoice"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/components/schemas/InvoiceType",
|
||||||
|
"description": "The type of invoice"
|
||||||
|
},
|
||||||
"checkoutLink": {
|
"checkoutLink": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The link to the checkout page, where you can redirect the customer"
|
"description": "The link to the checkout page, where you can redirect the customer"
|
||||||
@@ -972,6 +976,12 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "decimal",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The amount of the invoice. If null or unspecified, the invoice will be a top-up invoice. (ie. The invoice will consider any payment as a full payment)"
|
||||||
|
},
|
||||||
"additionalSearchTerms": {
|
"additionalSearchTerms": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -1172,6 +1182,18 @@
|
|||||||
"Processing",
|
"Processing",
|
||||||
"Settled"
|
"Settled"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"InvoiceType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "",
|
||||||
|
"x-enumNames": [
|
||||||
|
"Standard",
|
||||||
|
"TopUp"
|
||||||
|
],
|
||||||
|
"enum": [
|
||||||
|
"Standard",
|
||||||
|
"TopUp"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user