diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 3eb98b64a..e9e7bcffc 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Filters; +using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Changelly; @@ -439,15 +440,18 @@ namespace BTCPayServer.Controllers var list = await ListInvoicesProcess(searchTerm, skip, count); foreach (var invoice in list) { + var state = invoice.GetInvoiceState(); model.Invoices.Add(new InvoiceModel() { - Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"), + Status = state.ToString(), ShowCheckout = invoice.Status == "new", Date = invoice.InvoiceTime, InvoiceId = invoice.Id, OrderId = invoice.OrderId ?? string.Empty, RedirectUrl = invoice.RedirectURL ?? string.Empty, - AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}" + AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}", + CanMarkInvalid = state.CanMarkInvalid(), + CanMarkComplete = state.CanMarkComplete() }); } return View(model); @@ -587,11 +591,40 @@ namespace BTCPayServer.Controllers }); } - [HttpPost] - [Route("invoices/invalidatepaid")] + [HttpGet] + [Route("invoices/{invoiceId}/changestate/{newState}")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [BitpayAPIConstraint(false)] - public async Task InvalidatePaidInvoice(string invoiceId) + public IActionResult ChangeInvoiceState(string invoiceId, string newState) + { + if (newState == "invalid") + { + return View("Confirm", new ConfirmModel() + { + Action = "Make invoice invalid", + Title = "Change invoice state", + Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?", + }); + } + else if (newState == "complete") + { + return View("Confirm", new ConfirmModel() + { + Action = "Make invoice complete", + Title = "Change invoice state", + Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?", + ButtonClass = "btn-primary" + }); + } + else + return NotFound(); + } + + [HttpPost] + [Route("invoices/{invoiceId}/changestate/{newState}")] + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] + [BitpayAPIConstraint(false)] + public async Task ChangeInvoiceStateConfirm(string invoiceId, string newState) { var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() { @@ -600,8 +633,18 @@ namespace BTCPayServer.Controllers })).FirstOrDefault(); if (invoice == null) return NotFound(); - await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); - _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid")); + if (newState == "invalid") + { + await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); + _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid")); + StatusMessage = "Invoice marked invalid"; + } + else if(newState == "complete") + { + await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId); + _EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete")); + StatusMessage = "Invoice marked complete"; + } return RedirectToAction(nameof(ListInvoices)); } diff --git a/BTCPayServer/Data/InvoiceData.cs b/BTCPayServer/Data/InvoiceData.cs index 10d81c26a..65923631a 100644 --- a/BTCPayServer/Data/InvoiceData.cs +++ b/BTCPayServer/Data/InvoiceData.cs @@ -81,5 +81,10 @@ namespace BTCPayServer.Data get; set; } public List PendingInvoices { get; set; } + + public Services.Invoices.InvoiceState GetInvoiceState() + { + return new Services.Invoices.InvoiceState() { Status = Status, ExceptionStatus = ExceptionStatus }; + } } } diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 4a7e9fb24..455050373 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -345,6 +345,7 @@ namespace BTCPayServer.HostedServices e.Name == "invoice_paidInFull" || e.Name == "invoice_failedToConfirm" || e.Name == "invoice_markedInvalid" || + e.Name == "invoice_markedComplete" || e.Name == "invoice_failedToConfirm" || e.Name == "invoice_completed" || e.Name == "invoice_expiredPaidPartial" diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 5dfdd5260..7a4e3fb9c 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -46,6 +46,9 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } + public bool CanMarkComplete { get; set; } + public bool CanMarkInvalid { get; set; } + public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid; public bool ShowCheckout { get; set; } public string ExceptionStatus { get; set; } public string AmountCurrency diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 76277659a..b37bf0868 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -526,6 +526,42 @@ namespace BTCPayServer.Services.Invoices } #pragma warning restore CS0618 } + + public InvoiceState GetInvoiceState() + { + return new InvoiceState() { Status = Status, ExceptionStatus = ExceptionStatus }; + } + } + + public class InvoiceState + { + public string Status { get; set; } + public string ExceptionStatus { get; set; } + public bool CanMarkComplete() + { + return (Status == "paid") || +#pragma warning disable CA1305 // Specify IFormatProvider + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidPartial") || + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidLate") || + (Status != "complete" && ExceptionStatus?.ToString() == "marked") || + (Status == "invalid"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + + public bool CanMarkInvalid() + { + return (Status == "paid") || + (Status == "new") || +#pragma warning disable CA1305 // Specify IFormatProvider + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidPartial") || + ((Status == "new" || Status == "expired") && ExceptionStatus?.ToString() == "paidLate") || + (Status != "invalid" && ExceptionStatus?.ToString() == "marked"); +#pragma warning restore CA1305 // Specify IFormatProvider; + } + public override string ToString() + { + return Status + (ExceptionStatus == null ? string.Empty : $" ({ExceptionStatus})"); + } } public class PaymentMethodAccounting diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index bccba17f6..0a846f10d 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -334,9 +334,22 @@ namespace BTCPayServer.Services.Invoices using (var context = _ContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData?.Status != "paid") + if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid()) return; invoiceData.Status = "invalid"; + invoiceData.ExceptionStatus = "marked"; + await context.SaveChangesAsync().ConfigureAwait(false); + } + } + public async Task UpdatePaidInvoiceToComplete(string invoiceId) + { + using (var context = _ContextFactory.CreateContext()) + { + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete()) + return; + invoiceData.Status = "complete"; + invoiceData.ExceptionStatus = "marked"; await context.SaveChangesAsync().ConfigureAwait(false); } } diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 9c1061389..a415e5706 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -99,23 +99,7 @@ } @invoice.InvoiceId - @if (invoice.Status == "paid") - { - - - - } - else - { - @invoice.Status - } + @invoice.Status @invoice.AmountCurrency @if (invoice.ShowCheckout) @@ -123,9 +107,32 @@ Checkout [^] - - + @if (!invoice.CanMarkStatus) + { + - + } } + @if (invoice.CanMarkStatus) + { + + + } Details @@ -153,28 +160,3 @@ - - -