Skip to content

Commit

Permalink
Payment Request: Add processing status for on-chain payments
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisreimann committed Sep 11, 2023
1 parent 57bc90a commit 66d556a
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 43 deletions.
5 changes: 3 additions & 2 deletions BTCPayServer.Client/Models/PaymentRequestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
public class PaymentRequestData : PaymentRequestBaseData
{
[JsonConverter(typeof(StringEnumConverter))]
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
public PaymentRequestStatus Status { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; }
public string Id { get; set; }
Expand All @@ -16,7 +16,8 @@ public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
Expired = 2,
Processing = 3
}
}
}
38 changes: 33 additions & 5 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,13 +1159,13 @@ public async Task CanCreatePayRequest()
await s.StartAsync();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();

s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys(".01");

var currencyInput = s.Driver.FindElement(By.Id("Currency"));
Assert.Equal("USD", currencyInput.GetAttribute("value"));
Expand Down Expand Up @@ -1208,9 +1208,7 @@ public async Task CanCreatePayRequest()

// test invoice creation, click with JS, because the button is inside a sticky header
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
// checkout v1
s.Driver.WaitForElement(By.CssSelector("invoice"));
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));

// amount and currency should not be editable, because invoice exists
s.GoToUrl(editUrl);
Expand All @@ -1231,6 +1229,36 @@ public async Task CanCreatePayRequest()
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
Assert.Contains("Pay123", s.Driver.PageSource);

// payment
s.GoToUrl(viewUrl);
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");

// Pay full amount
s.PayInvoice();

// Processing
TestUtils.Eventually(() =>
{
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Received", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
});

s.GoToUrl(viewUrl);
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
s.Driver.Navigate().Back();

// Mine
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
s.GoToUrl(viewUrl);
Assert.Equal("Settled", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
}

[Fact(Timeout = TestTimeout)]
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer/Controllers/UIPaymentRequestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public async Task<IActionResult> EditPaymentRequest(string payReqId, UpdatePayme
_EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, });

TempData[WellKnownTempData.SuccessMessage] = $"Payment request \"{viewModel.Title}\" {(isNewPaymentRequest ? "created" : "updated")} successfully";
return RedirectToAction(nameof(GetPaymentRequests), new { storeId = store.Id, payReqId = data.Id });
return RedirectToAction(nameof(GetPaymentRequests), new { storeId = store.Id });
}

[HttpGet("{payReqId}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ public ViewPaymentRequestViewModel(PaymentRequestData data)
Status = "Pending";
IsPending = true;
break;
case Client.Models.PaymentRequestData.PaymentRequestStatus.Processing:
Status = "Processing";
break;
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
Status = "Settled";
break;
Expand Down
29 changes: 22 additions & 7 deletions BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class PaymentRequestHub : Hub
{
private readonly UIPaymentRequestController _PaymentRequestController;
public const string InvoiceCreated = "InvoiceCreated";
public const string InvoiceConfirmed = "InvoiceConfirmed";
public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError";
Expand Down Expand Up @@ -128,9 +129,13 @@ public override async Task StartAsync(CancellationToken cancellationToken)
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
{
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
var items = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
var items = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
{
Status = new[] { Client.Models.PaymentRequestData.PaymentRequestStatus.Pending }
Status = new[]
{
PaymentRequestData.PaymentRequestStatus.Pending,
PaymentRequestData.PaymentRequestStatus.Processing
}
}, cancellationToken);
Logs.PayServer.LogInformation($"{items.Length} pending payment requests being checked since last run");
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
Expand All @@ -157,7 +162,7 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
{
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment || invoiceEvent.Name == InvoiceEvent.MarkedCompleted || invoiceEvent.Name == InvoiceEvent.MarkedInvalid)
if (invoiceEvent.Name is InvoiceEvent.ReceivedPayment or InvoiceEvent.MarkedCompleted or InvoiceEvent.MarkedInvalid)
{
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
var data = invoiceEvent.Payment?.GetCryptoPaymentData();
Expand All @@ -168,10 +173,19 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
{
data.GetValue(),
invoiceEvent.Payment.Currency,
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType.ToString()
}, cancellationToken);
}
}
else if (invoiceEvent.Name is InvoiceEvent.Completed or InvoiceEvent.Confirmed)
{
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.InvoiceConfirmed,
new object[]
{
invoiceEvent.InvoiceId
}, cancellationToken);
}

await InfoUpdated(paymentId);
}
Expand All @@ -181,10 +195,11 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.PaymentRequestId);
await InfoUpdated(updated.PaymentRequestId);

var isPending = updated.Data.Status is
PaymentRequestData.PaymentRequestStatus.Pending or
PaymentRequestData.PaymentRequestStatus.Processing;
var expiry = updated.Data.GetBlob().ExpiryDate;
if (updated.Data.Status ==
PaymentRequestData.PaymentRequestStatus.Pending &&
expiry.HasValue)
if (isPending && expiry.HasValue)
{
QueueExpiryTask(
updated.PaymentRequestId,
Expand Down
20 changes: 13 additions & 7 deletions BTCPayServer/PaymentRequest/PaymentRequestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;

namespace BTCPayServer.PaymentRequest
Expand All @@ -27,7 +26,6 @@ public class PaymentRequestService
PaymentRequestRepository paymentRequestRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository,
AppService appService,
DisplayFormatter displayFormatter,
CurrencyNameTable currencies)
{
Expand Down Expand Up @@ -62,10 +60,19 @@ public async Task UpdatePaymentRequestStateIfNeeded(PaymentRequestData pr)
{
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
var allSettled = contributions.All(i => i.Value.States.All(s => s.IsSettled()));
var isPaid = contributions.TotalCurrency >= blob.Amount;

currentStatus = contributions.TotalCurrency >= blob.Amount
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
: Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
if (isPaid)
{
currentStatus = allSettled
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
: Client.Models.PaymentRequestData.PaymentRequestStatus.Processing;
}
else
{
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
}
}

if (currentStatus != pr.Status)
Expand All @@ -86,12 +93,11 @@ public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, stri
var blob = pr.GetBlob();

var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);

var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
var amountDue = blob.Amount - paymentStats.TotalCurrency;
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);

return new ViewPaymentRequestViewModel(pr)
{
Archived = pr.Archived,
Expand Down
10 changes: 9 additions & 1 deletion BTCPayServer/Services/Invoices/InvoiceEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,14 @@ public bool CanRefund()
Status == InvoiceStatusLegacy.Invalid;
}

public bool IsSettled()
{
return Status == InvoiceStatusLegacy.Confirmed ||
Status == InvoiceStatusLegacy.Complete ||
(Status == InvoiceStatusLegacy.Expired &&
ExceptionStatus is InvoiceExceptionStatus.PaidLate or InvoiceExceptionStatus.PaidOver);
}

public override int GetHashCode()
{
return HashCode.Combine(Status, ExceptionStatus);
Expand Down Expand Up @@ -970,7 +978,7 @@ public override bool Equals(object obj)
}
public override string ToString()
{
return Status.ToModernStatus().ToString() + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
return Status.ToModernStatus() + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
}
}

Expand Down
24 changes: 16 additions & 8 deletions BTCPayServer/Services/Invoices/InvoiceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -780,9 +780,12 @@ public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, Invo
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
.SelectMany(p =>
{
var contribution = new InvoiceStatistics.Contribution();
contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike);
contribution.CurrencyValue = p.Price;
var contribution = new InvoiceStatistics.Contribution
{
PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike),
CurrencyValue = p.Price,
States = new [] { p.GetInvoiceState() }
};
contribution.Value = contribution.CurrencyValue;
// For hardcap, we count newly created invoices as part of the contributions
Expand All @@ -809,18 +812,22 @@ public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, Invo
return payments
.Select(pay =>
{
var paymentMethodContribution = new InvoiceStatistics.Contribution();
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net;
paymentMethodContribution.Value = pay.PaidAmount.Net;
var paymentMethodContribution = new InvoiceStatistics.Contribution
{
PaymentMethodId = pay.GetPaymentMethodId(),
CurrencyValue = pay.InvoicePaidAmount.Net,
Value = pay.PaidAmount.Net,
States = new [] { pay.InvoiceEntity.GetInvoiceState() }
};
return paymentMethodContribution;
})
.ToArray();
})
.GroupBy(p => p.PaymentMethodId)
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution()
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution
{
PaymentMethodId = p.Key,
States = p.SelectMany(v => v.States),
Value = p.Select(v => v.Value).Sum(),
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
});
Expand Down Expand Up @@ -907,6 +914,7 @@ public InvoiceStatistics(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>
public class Contribution
{
public PaymentMethodId PaymentMethodId { get; set; }
public IEnumerable<InvoiceState> States { get; set; }
public decimal Value { get; set; }
public decimal CurrencyValue { get; set; }
}
Expand Down
Loading

0 comments on commit 66d556a

Please sign in to comment.