Disabled amount/currency update for payment request with active invoices (#4390)

* Disabled amount/currency update for payment request with active invoices

close #4241

* Check amount isn't changed in backend

* Add test case

* Update BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs

Co-authored-by: d11n <mail@dennisreimann.de>

* Update BTCPayServer/Controllers/UIPaymentRequestController.cs

Co-authored-by: d11n <mail@dennisreimann.de>

* Improve wording

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Umar Bolatov
2022-12-13 21:01:48 -08:00
committed by GitHub
parent 6972e8a3db
commit 06cedaef4b
8 changed files with 63 additions and 18 deletions

View File

@@ -1409,6 +1409,12 @@ namespace BTCPayServer.Tests
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()); var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
await Pay(invoiceData.Id); await Pay(invoiceData.Id);
// Can't update amount once invoice has been created
await AssertValidationError(new[] { "Amount" }, () => client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 294m
}));
// Let's tests some unhappy path // Let's tests some unhappy path
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId, paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" }); new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });

View File

@@ -53,7 +53,7 @@ namespace BTCPayServer.Tests
// Permission guard for guests editing // Permission guard for guests editing
Assert Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); .IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
request.Title = "update"; request.Title = "update";
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request)); Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));

View File

@@ -943,6 +943,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SaveButton")).Click(); s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click(); s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
// amount and currency should be editable, because no invoice exists
s.GoToUrl(editUrl);
Assert.True(s.Driver.FindElement(By.Id("Amount")).Enabled);
Assert.True(s.Driver.FindElement(By.Id("Currency")).Enabled);
s.GoToUrl(viewUrl); s.GoToUrl(viewUrl);
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']")); s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim()); Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
@@ -953,8 +958,12 @@ namespace BTCPayServer.Tests
s.Driver.WaitForElement(By.CssSelector("invoice")); s.Driver.WaitForElement(By.CssSelector("invoice"));
Assert.Contains("Awaiting Payment", s.Driver.PageSource); Assert.Contains("Awaiting Payment", s.Driver.PageSource);
// archive (from details page) // amount and currency should not be editable, because invoice exists
s.GoToUrl(editUrl); s.GoToUrl(editUrl);
Assert.False(s.Driver.FindElement(By.Id("Amount")).Enabled);
Assert.False(s.Driver.FindElement(By.Id("Currency")).Enabled);
// archive (from details page)
var payReqId = s.Driver.Url.Split('/').Last(); var payReqId = s.Driver.Url.Split('/').Last();
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click(); s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text); Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection.Metadata;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
@@ -15,6 +16,7 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
@@ -30,6 +32,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly UIInvoiceController _invoiceController; private readonly UIInvoiceController _invoiceController;
private readonly PaymentRequestRepository _paymentRequestRepository; private readonly PaymentRequestRepository _paymentRequestRepository;
private readonly CurrencyNameTable _currencyNameTable; private readonly CurrencyNameTable _currencyNameTable;
private readonly UserManager<ApplicationUser> _userManager;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
public GreenfieldPaymentRequestsController( public GreenfieldPaymentRequestsController(
@@ -38,6 +41,7 @@ namespace BTCPayServer.Controllers.Greenfield
PaymentRequestRepository paymentRequestRepository, PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService, PaymentRequestService paymentRequestService,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
LinkGenerator linkGenerator) LinkGenerator linkGenerator)
{ {
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
@@ -45,6 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield
_paymentRequestRepository = paymentRequestRepository; _paymentRequestRepository = paymentRequestRepository;
PaymentRequestService = paymentRequestService; PaymentRequestService = paymentRequestService;
_currencyNameTable = currencyNameTable; _currencyNameTable = currencyNameTable;
_userManager = userManager;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
} }
@@ -152,7 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> CreatePaymentRequest(string storeId, public async Task<IActionResult> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request) CreatePaymentRequestRequest request)
{ {
var validationResult = Validate(request); var validationResult = await Validate(null, request);
if (validationResult != null) if (validationResult != null)
{ {
return validationResult; return validationResult;
@@ -178,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdatePaymentRequest(string storeId, public async Task<IActionResult> UpdatePaymentRequest(string storeId,
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request) string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
{ {
var validationResult = Validate(request); var validationResult = await Validate(paymentRequestId, request);
if (validationResult != null) if (validationResult != null)
{ {
return validationResult; return validationResult;
@@ -196,11 +201,22 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr))); return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
} }
private string GetUserId() => _userManager.GetUserId(User);
private IActionResult Validate(PaymentRequestBaseData data) private async Task<IActionResult> Validate(string id, PaymentRequestBaseData data)
{ {
if (data is null) if (data is null)
return BadRequest(); return BadRequest();
if (id != null)
{
var pr = await this.PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (pr.Amount != data.Amount)
{
if (pr.Invoices.Any())
ModelState.AddModelError(nameof(data.Amount), "Amount and currency are not editable once payment request has invoices");
}
}
if (data.Amount <= 0) if (data.Amount <= 0)
{ {
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0"); ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");

View File

@@ -93,7 +93,7 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")] [HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
public IActionResult EditPaymentRequest(string storeId, string payReqId) public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{ {
var store = GetCurrentStore(); var store = GetCurrentStore();
var paymentRequest = GetCurrentPaymentRequest(); var paymentRequest = GetCurrentPaymentRequest();
@@ -102,9 +102,11 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
} }
var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
var vm = new UpdatePaymentRequestViewModel(paymentRequest) var vm = new UpdatePaymentRequestViewModel(paymentRequest)
{ {
StoreId = store.Id StoreId = store.Id,
AmountAndCurrencyEditable = payReqId is null || !prInvoices.Any()
}; };
vm.Currency ??= store.GetStoreBlob().DefaultCurrency; vm.Currency ??= store.GetStoreBlob().DefaultCurrency;
@@ -131,17 +133,24 @@ namespace BTCPayServer.Controllers
{ {
ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request."); ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request.");
} }
var data = paymentRequest ?? new PaymentRequestData();
data.StoreDataId = viewModel.StoreId;
data.Archived = viewModel.Archived;
var blob = data.GetBlob();
if (blob.Amount != viewModel.Amount && payReqId != null)
{
var prInvoices = (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
if (prInvoices.Any())
ModelState.AddModelError(nameof(viewModel.Amount), "Amount and currency are not editable once payment request has invoices");
}
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(nameof(EditPaymentRequest), viewModel); return View(nameof(EditPaymentRequest), viewModel);
} }
var data = paymentRequest ?? new PaymentRequestData();
data.StoreDataId = viewModel.StoreId;
data.Archived = viewModel.Archived;
var blob = data.GetBlob();
blob.Title = viewModel.Title; blob.Title = viewModel.Title;
blob.Email = viewModel.Email; blob.Email = viewModel.Email;
blob.Description = viewModel.Description; blob.Description = viewModel.Description;
@@ -343,10 +352,10 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("{payReqId}/clone")] [HttpGet("{payReqId}/clone")]
public IActionResult ClonePaymentRequest(string payReqId) public async Task<IActionResult> ClonePaymentRequest(string payReqId)
{ {
var store = GetCurrentStore(); var store = GetCurrentStore();
var result = EditPaymentRequest(store.Id, payReqId); var result = await EditPaymentRequest(store.Id, payReqId);
if (result is ViewResult viewResult) if (result is ViewResult viewResult)
{ {
var model = (UpdatePaymentRequestViewModel)viewResult.Model; var model = (UpdatePaymentRequestViewModel)viewResult.Model;
@@ -364,7 +373,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId) public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
{ {
var store = GetCurrentStore(); var store = GetCurrentStore();
var result = EditPaymentRequest(store.Id, payReqId); var result = await EditPaymentRequest(store.Id, payReqId);
if (result is ViewResult viewResult) if (result is ViewResult viewResult)
{ {
var model = (UpdatePaymentRequestViewModel)viewResult.Model; var model = (UpdatePaymentRequestViewModel)viewResult.Model;

View File

@@ -87,6 +87,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public bool AllowCustomPaymentAmounts { get; set; } public bool AllowCustomPaymentAmounts { get; set; }
public Dictionary<string, object> FormResponse { get; set; } public Dictionary<string, object> FormResponse { get; set; }
public bool AmountAndCurrencyEditable { get; set; } = true;
} }
public class ViewPaymentRequestViewModel public class ViewPaymentRequestViewModel

View File

@@ -72,7 +72,7 @@ namespace BTCPayServer.PaymentRequest
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null) public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
{ {
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null); var pr = await _PaymentRequestRepository.FindPaymentRequest(id, userId);
if (pr == null) if (pr == null)
{ {
return null; return null;

View File

@@ -49,12 +49,16 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="form-group flex-fill me-4"> <div class="form-group flex-fill me-4">
<label asp-for="Amount" class="form-label" data-required></label> <label asp-for="Amount" class="form-label" data-required></label>
<input type="number" inputmode="decimal" step="any" asp-for="Amount" class="form-control" required /> <input type="number" inputmode="decimal" step="any" asp-for="Amount" class="form-control" required disabled="@(!Model.AmountAndCurrencyEditable)" />
<span asp-validation-for="Amount" class="text-danger"></span> <span asp-validation-for="Amount" class="text-danger"></span>
@if (!Model.AmountAndCurrencyEditable)
{
<p class="text-warning mb-0 mt-2">Amount and currency are not editable once payment request has invoices</p>
}
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Currency" class="form-label"></label> <label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control w-auto" currency-selection /> <input asp-for="Currency" class="form-control w-auto" currency-selection disabled="@(!Model.AmountAndCurrencyEditable)" />
<span asp-validation-for="Currency" class="text-danger"></span> <span asp-validation-for="Currency" class="text-danger"></span>
</div> </div>
</div> </div>