diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index df1a72ed4..8e0a48011 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1409,6 +1409,12 @@ namespace BTCPayServer.Tests var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()); 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 paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" }); diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs index 50714d9db..986f0c067 100644 --- a/BTCPayServer.Tests/PaymentRequestTests.cs +++ b/BTCPayServer.Tests/PaymentRequestTests.cs @@ -53,7 +53,7 @@ namespace BTCPayServer.Tests // Permission guard for guests editing Assert - .IsType(guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); + .IsType(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); request.Title = "update"; Assert.IsType(await paymentRequestController.EditPaymentRequest(id, request)); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index e0f3a3a15..609c7d66b 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -942,6 +942,11 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("ClearExpiryDate")).Click(); s.Driver.FindElement(By.Id("SaveButton")).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.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']")); @@ -953,8 +958,12 @@ namespace BTCPayServer.Tests s.Driver.WaitForElement(By.CssSelector("invoice")); Assert.Contains("Awaiting Payment", s.Driver.PageSource); - // archive (from details page) + // amount and currency should not be editable, because invoice exists 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(); s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click(); Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs index ac345fe98..57fdd9b16 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; @@ -15,6 +16,7 @@ using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; @@ -30,6 +32,7 @@ namespace BTCPayServer.Controllers.Greenfield private readonly UIInvoiceController _invoiceController; private readonly PaymentRequestRepository _paymentRequestRepository; private readonly CurrencyNameTable _currencyNameTable; + private readonly UserManager _userManager; private readonly LinkGenerator _linkGenerator; public GreenfieldPaymentRequestsController( @@ -38,6 +41,7 @@ namespace BTCPayServer.Controllers.Greenfield PaymentRequestRepository paymentRequestRepository, PaymentRequestService paymentRequestService, CurrencyNameTable currencyNameTable, + UserManager userManager, LinkGenerator linkGenerator) { _InvoiceRepository = invoiceRepository; @@ -45,6 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield _paymentRequestRepository = paymentRequestRepository; PaymentRequestService = paymentRequestService; _currencyNameTable = currencyNameTable; + _userManager = userManager; _linkGenerator = linkGenerator; } @@ -152,7 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield public async Task CreatePaymentRequest(string storeId, CreatePaymentRequestRequest request) { - var validationResult = Validate(request); + var validationResult = await Validate(null, request); if (validationResult != null) { return validationResult; @@ -178,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield public async Task UpdatePaymentRequest(string storeId, string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request) { - var validationResult = Validate(request); + var validationResult = await Validate(paymentRequestId, request); if (validationResult != null) { return validationResult; @@ -196,11 +201,22 @@ namespace BTCPayServer.Controllers.Greenfield return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr))); } + private string GetUserId() => _userManager.GetUserId(User); - private IActionResult Validate(PaymentRequestBaseData data) + private async Task Validate(string id, PaymentRequestBaseData data) { if (data is null) 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) { ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0"); diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs index 277c5f800..c3695b188 100644 --- a/BTCPayServer/Controllers/UIPaymentRequestController.cs +++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs @@ -93,7 +93,7 @@ namespace BTCPayServer.Controllers } [HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")] - public IActionResult EditPaymentRequest(string storeId, string payReqId) + public async Task EditPaymentRequest(string storeId, string payReqId) { var store = GetCurrentStore(); var paymentRequest = GetCurrentPaymentRequest(); @@ -102,9 +102,11 @@ namespace BTCPayServer.Controllers return NotFound(); } + var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices; var vm = new UpdatePaymentRequestViewModel(paymentRequest) { - StoreId = store.Id + StoreId = store.Id, + AmountAndCurrencyEditable = payReqId is null || !prInvoices.Any() }; vm.Currency ??= store.GetStoreBlob().DefaultCurrency; @@ -131,17 +133,24 @@ namespace BTCPayServer.Controllers { 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) { 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.Email = viewModel.Email; blob.Description = viewModel.Description; @@ -343,10 +352,10 @@ namespace BTCPayServer.Controllers } [HttpGet("{payReqId}/clone")] - public IActionResult ClonePaymentRequest(string payReqId) + public async Task ClonePaymentRequest(string payReqId) { var store = GetCurrentStore(); - var result = EditPaymentRequest(store.Id, payReqId); + var result = await EditPaymentRequest(store.Id, payReqId); if (result is ViewResult viewResult) { var model = (UpdatePaymentRequestViewModel)viewResult.Model; @@ -364,7 +373,7 @@ namespace BTCPayServer.Controllers public async Task TogglePaymentRequestArchival(string payReqId) { var store = GetCurrentStore(); - var result = EditPaymentRequest(store.Id, payReqId); + var result = await EditPaymentRequest(store.Id, payReqId); if (result is ViewResult viewResult) { var model = (UpdatePaymentRequestViewModel)viewResult.Model; diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index 58a81de1b..812e21742 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -87,6 +87,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels public bool AllowCustomPaymentAmounts { get; set; } public Dictionary FormResponse { get; set; } + public bool AmountAndCurrencyEditable { get; set; } = true; } public class ViewPaymentRequestViewModel diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index d14d2cd50..8c69f8ef1 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -72,7 +72,7 @@ namespace BTCPayServer.PaymentRequest public async Task GetPaymentRequest(string id, string userId = null) { - var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null); + var pr = await _PaymentRequestRepository.FindPaymentRequest(id, userId); if (pr == null) { return null; diff --git a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml index 56c8f39ef..7e87f39be 100644 --- a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml +++ b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml @@ -49,12 +49,16 @@
- + + @if (!Model.AmountAndCurrencyEditable) + { +

Amount and currency are not editable once payment request has invoices

+ }
- +