mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Payment Request: Add processing status for on-chain payments (#5309)
Closes #5297.
This commit is contained in:
@@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
public class PaymentRequestData : PaymentRequestBaseData
|
public class PaymentRequestData : PaymentRequestBaseData
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
public PaymentRequestStatus Status { get; set; }
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
public DateTimeOffset CreatedTime { get; set; }
|
public DateTimeOffset CreatedTime { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@@ -16,7 +16,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
Completed = 1,
|
Completed = 1,
|
||||||
Expired = 2
|
Expired = 2,
|
||||||
|
Processing = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1606,7 +1606,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
await user.MakeAdmin();
|
await user.MakeAdmin();
|
||||||
var client = await user.CreateClient(Policies.Unrestricted);
|
var client = await user.CreateClient(Policies.Unrestricted);
|
||||||
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
||||||
@@ -1688,11 +1688,18 @@ namespace BTCPayServer.Tests
|
|||||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||||
});
|
});
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
if (!partialPayment)
|
if (!partialPayment)
|
||||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
});
|
});
|
||||||
|
await tester.ExplorerNode.GenerateAsync(1);
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
|
if (!partialPayment)
|
||||||
|
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await Pay(invoiceId);
|
await Pay(invoiceId);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ namespace BTCPayServer.Tests
|
|||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
var user2 = tester.NewAccount();
|
var user2 = tester.NewAccount();
|
||||||
|
|
||||||
await user2.GrantAccessAsync();
|
await user2.GrantAccessAsync();
|
||||||
|
|
||||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||||
@@ -162,7 +161,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||||
@@ -170,7 +169,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.IsType<NotFoundResult>(await
|
Assert.IsType<NotFoundResult>(await
|
||||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||||
|
|
||||||
var request = new UpdatePaymentRequestViewModel()
|
var request = new UpdatePaymentRequestViewModel
|
||||||
{
|
{
|
||||||
Title = "original juice",
|
Title = "original juice",
|
||||||
Currency = "BTC",
|
Currency = "BTC",
|
||||||
|
|||||||
@@ -1159,13 +1159,13 @@ namespace BTCPayServer.Tests
|
|||||||
await s.StartAsync();
|
await s.StartAsync();
|
||||||
s.RegisterNewUser();
|
s.RegisterNewUser();
|
||||||
s.CreateNewStore();
|
s.CreateNewStore();
|
||||||
s.EnableCheckout(CheckoutType.V1);
|
|
||||||
s.AddDerivationScheme();
|
s.AddDerivationScheme();
|
||||||
|
|
||||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
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"));
|
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
|
||||||
Assert.Equal("USD", currencyInput.GetAttribute("value"));
|
Assert.Equal("USD", currencyInput.GetAttribute("value"));
|
||||||
@@ -1208,9 +1208,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
// test invoice creation, click with JS, because the button is inside a sticky header
|
// test invoice creation, click with JS, because the button is inside a sticky header
|
||||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||||
// checkout v1
|
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||||
s.Driver.WaitForElement(By.CssSelector("invoice"));
|
|
||||||
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
|
|
||||||
|
|
||||||
// amount and currency should not be editable, because invoice exists
|
// amount and currency should not be editable, because invoice exists
|
||||||
s.GoToUrl(editUrl);
|
s.GoToUrl(editUrl);
|
||||||
@@ -1231,6 +1229,36 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||||
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
||||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
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)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ namespace BTCPayServer.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var store = await _storeRepository.FindStore(result.StoreId);
|
var store = await _storeRepository.FindStore(result.StoreId);
|
||||||
var prData = await _PaymentRequestRepository.FindPaymentRequest(result.Id, null);
|
var prData = await _PaymentRequestRepository.FindPaymentRequest(result.Id, null, cancellationToken);
|
||||||
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(prData, amount, result.AmountDue, store, Request, cancellationToken);
|
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(prData, amount, result.AmountDue, store, Request, cancellationToken);
|
||||||
if (redirectToInvoice)
|
if (redirectToInvoice)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||||||
Status = "Pending";
|
Status = "Pending";
|
||||||
IsPending = true;
|
IsPending = true;
|
||||||
break;
|
break;
|
||||||
|
case Client.Models.PaymentRequestData.PaymentRequestStatus.Processing:
|
||||||
|
Status = "Processing";
|
||||||
|
break;
|
||||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
|
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
|
||||||
Status = "Settled";
|
Status = "Settled";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
private readonly UIPaymentRequestController _PaymentRequestController;
|
private readonly UIPaymentRequestController _PaymentRequestController;
|
||||||
public const string InvoiceCreated = "InvoiceCreated";
|
public const string InvoiceCreated = "InvoiceCreated";
|
||||||
|
public const string InvoiceConfirmed = "InvoiceConfirmed";
|
||||||
public const string PaymentReceived = "PaymentReceived";
|
public const string PaymentReceived = "PaymentReceived";
|
||||||
public const string InfoUpdated = "InfoUpdated";
|
public const string InfoUpdated = "InfoUpdated";
|
||||||
public const string InvoiceError = "InvoiceError";
|
public const string InvoiceError = "InvoiceError";
|
||||||
@@ -128,9 +129,13 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
|
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);
|
}, cancellationToken);
|
||||||
Logs.PayServer.LogInformation($"{items.Length} pending payment requests being checked since last run");
|
Logs.PayServer.LogInformation($"{items.Length} pending payment requests being checked since last run");
|
||||||
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
||||||
@@ -157,7 +162,7 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
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);
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
|
||||||
var data = invoiceEvent.Payment?.GetCryptoPaymentData();
|
var data = invoiceEvent.Payment?.GetCryptoPaymentData();
|
||||||
@@ -168,10 +173,19 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
data.GetValue(),
|
data.GetValue(),
|
||||||
invoiceEvent.Payment.Currency,
|
invoiceEvent.Payment.Currency,
|
||||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType.ToString()
|
||||||
}, cancellationToken);
|
}, 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);
|
await InfoUpdated(paymentId);
|
||||||
}
|
}
|
||||||
@@ -181,10 +195,11 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.PaymentRequestId);
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.PaymentRequestId);
|
||||||
await InfoUpdated(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;
|
var expiry = updated.Data.GetBlob().ExpiryDate;
|
||||||
if (updated.Data.Status ==
|
if (isPending && expiry.HasValue)
|
||||||
PaymentRequestData.PaymentRequestStatus.Pending &&
|
|
||||||
expiry.HasValue)
|
|
||||||
{
|
{
|
||||||
QueueExpiryTask(
|
QueueExpiryTask(
|
||||||
updated.PaymentRequestId,
|
updated.PaymentRequestId,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using BTCPayServer.Services.Apps;
|
|||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.PaymentRequests;
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||||
|
|
||||||
namespace BTCPayServer.PaymentRequest
|
namespace BTCPayServer.PaymentRequest
|
||||||
@@ -27,7 +26,6 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
PaymentRequestRepository paymentRequestRepository,
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
AppService appService,
|
|
||||||
DisplayFormatter displayFormatter,
|
DisplayFormatter displayFormatter,
|
||||||
CurrencyNameTable currencies)
|
CurrencyNameTable currencies)
|
||||||
{
|
{
|
||||||
@@ -62,10 +60,19 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||||
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
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
|
if (isPaid)
|
||||||
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
{
|
||||||
: Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
|
currentStatus = allSettled
|
||||||
|
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
||||||
|
: Client.Models.PaymentRequestData.PaymentRequestStatus.Processing;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStatus != pr.Status)
|
if (currentStatus != pr.Status)
|
||||||
@@ -86,12 +93,11 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
var blob = pr.GetBlob();
|
var blob = pr.GetBlob();
|
||||||
|
|
||||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||||
|
|
||||||
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||||
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
||||||
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);
|
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);
|
||||||
|
|
||||||
return new ViewPaymentRequestViewModel(pr)
|
return new ViewPaymentRequestViewModel(pr)
|
||||||
{
|
{
|
||||||
Archived = pr.Archived,
|
Archived = pr.Archived,
|
||||||
|
|||||||
@@ -935,6 +935,14 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
Status == InvoiceStatusLegacy.Invalid;
|
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()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return HashCode.Combine(Status, ExceptionStatus);
|
return HashCode.Combine(Status, ExceptionStatus);
|
||||||
@@ -970,7 +978,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
}
|
}
|
||||||
public override string ToString()
|
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)})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -786,9 +786,12 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||||
.SelectMany(p =>
|
.SelectMany(p =>
|
||||||
{
|
{
|
||||||
var contribution = new InvoiceStatistics.Contribution();
|
var contribution = new InvoiceStatistics.Contribution
|
||||||
contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike);
|
{
|
||||||
contribution.CurrencyValue = p.Price;
|
PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike),
|
||||||
|
CurrencyValue = p.Price,
|
||||||
|
States = new [] { p.GetInvoiceState() }
|
||||||
|
};
|
||||||
contribution.Value = contribution.CurrencyValue;
|
contribution.Value = contribution.CurrencyValue;
|
||||||
|
|
||||||
// For hardcap, we count newly created invoices as part of the contributions
|
// For hardcap, we count newly created invoices as part of the contributions
|
||||||
@@ -815,18 +818,22 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return payments
|
return payments
|
||||||
.Select(pay =>
|
.Select(pay =>
|
||||||
{
|
{
|
||||||
var paymentMethodContribution = new InvoiceStatistics.Contribution();
|
var paymentMethodContribution = new InvoiceStatistics.Contribution
|
||||||
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
|
{
|
||||||
paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net;
|
PaymentMethodId = pay.GetPaymentMethodId(),
|
||||||
paymentMethodContribution.Value = pay.PaidAmount.Net;
|
CurrencyValue = pay.InvoicePaidAmount.Net,
|
||||||
|
Value = pay.PaidAmount.Net,
|
||||||
|
States = new [] { pay.InvoiceEntity.GetInvoiceState() }
|
||||||
|
};
|
||||||
return paymentMethodContribution;
|
return paymentMethodContribution;
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
})
|
})
|
||||||
.GroupBy(p => p.PaymentMethodId)
|
.GroupBy(p => p.PaymentMethodId)
|
||||||
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution()
|
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution
|
||||||
{
|
{
|
||||||
PaymentMethodId = p.Key,
|
PaymentMethodId = p.Key,
|
||||||
|
States = p.SelectMany(v => v.States),
|
||||||
Value = p.Select(v => v.Value).Sum(),
|
Value = p.Select(v => v.Value).Sum(),
|
||||||
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
|
||||||
});
|
});
|
||||||
@@ -913,6 +920,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
public class Contribution
|
public class Contribution
|
||||||
{
|
{
|
||||||
public PaymentMethodId PaymentMethodId { get; set; }
|
public PaymentMethodId PaymentMethodId { get; set; }
|
||||||
|
public IEnumerable<InvoiceState> States { get; set; }
|
||||||
public decimal Value { get; set; }
|
public decimal Value { get; set; }
|
||||||
public decimal CurrencyValue { get; set; }
|
public decimal CurrencyValue { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,25 +10,21 @@
|
|||||||
Layout = null;
|
Layout = null;
|
||||||
string StatusClass(InvoiceState state)
|
string StatusClass(InvoiceState state)
|
||||||
{
|
{
|
||||||
switch (state.Status.ToModernStatus())
|
var status = state.Status.ToModernStatus();
|
||||||
|
switch (status)
|
||||||
{
|
{
|
||||||
case InvoiceStatus.Settled:
|
|
||||||
case InvoiceStatus.Processing:
|
|
||||||
return "success";
|
|
||||||
case InvoiceStatus.Expired:
|
case InvoiceStatus.Expired:
|
||||||
switch (state.ExceptionStatus)
|
switch (state.ExceptionStatus)
|
||||||
{
|
{
|
||||||
case InvoiceExceptionStatus.PaidLate:
|
case InvoiceExceptionStatus.PaidLate:
|
||||||
case InvoiceExceptionStatus.PaidPartial:
|
case InvoiceExceptionStatus.PaidPartial:
|
||||||
case InvoiceExceptionStatus.PaidOver:
|
case InvoiceExceptionStatus.PaidOver:
|
||||||
return "warning";
|
return "unusual";
|
||||||
default:
|
default:
|
||||||
return "danger";
|
return "expired";
|
||||||
}
|
}
|
||||||
case InvoiceStatus.Invalid:
|
|
||||||
return "danger";
|
|
||||||
default:
|
default:
|
||||||
return "warning";
|
return status.ToString().ToLowerInvariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +127,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="h2 text-md-end">
|
<div class="h2 text-md-end">
|
||||||
<span class="badge @if (Model.Status == "Settled") { @("bg-primary") } else if (Model.Status == "Expired") { @("bg-danger") } else { @("bg-info") }" data-test="status">
|
<span class="badge badge-@Model.Status.ToLowerInvariant()" data-test="status" style="font-size:.75em">
|
||||||
@Model.Status
|
@Model.Status
|
||||||
@if (Model.Archived)
|
@if (Model.Archived)
|
||||||
{
|
{
|
||||||
@@ -186,7 +182,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="h2 text-md-end">
|
<div class="h2 text-md-end">
|
||||||
<span class="badge" :class="{ 'bg-primary': srvModel.status === 'Settled', 'bg-danger': srvModel.status === 'Expired', 'bg-info': (srvModel.status !== 'Settled' && srvModel.status !== 'Expired') }" data-test="status">
|
<span class="badge" :class="`badge-${srvModel.status.toLowerCase()}`" data-test="status" style="font-size:.75em">
|
||||||
{{srvModel.status}}
|
{{srvModel.status}}
|
||||||
<span v-if="srvModel.archived">(archived)</span>
|
<span v-if="srvModel.archived">(archived)</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -262,18 +258,18 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td>@invoice.Id</td>
|
<td>@invoice.Id</td>
|
||||||
<td>@invoice.ExpiryDate.ToString("g")</td>
|
<td>@invoice.ExpiryDate.ToString("g")</td>
|
||||||
<td class="text-end">@invoice.AmountFormatted</td>
|
<td class="text-end">@invoice.AmountFormatted</td>
|
||||||
<td class="text-end"></td>
|
<td class="text-end"></td>
|
||||||
<td class="text-end text-print-default">
|
<td class="text-end text-print-default">
|
||||||
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
|
<span class="badge badge-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@if (invoice.Payments != null && invoice.Payments.Any())
|
@if (invoice.Payments != null && invoice.Payments.Any())
|
||||||
{
|
{
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<th class="fw-normal text-secondary">Destination</th>
|
<th class="fw-normal text-secondary">Destination</th>
|
||||||
<th class="fw-normal text-secondary">Received</th>
|
<th class="fw-normal text-secondary">Received</th>
|
||||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||||
@@ -282,14 +278,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@foreach (var payment in invoice.Payments)
|
@foreach (var payment in invoice.Payments)
|
||||||
{
|
{
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td class="text-break"><code>@payment.Destination</code></td>
|
<td class="text-break"><code>@payment.Destination</code></td>
|
||||||
<td>@payment.ReceivedDate.ToString("g")</td>
|
<td>@payment.ReceivedDate.ToString("g")</td>
|
||||||
<td class="text-end">@payment.PaidFormatted</td>
|
<td class="text-end">@payment.PaidFormatted</td>
|
||||||
<td class="text-end">@payment.RateFormatted</td>
|
<td class="text-end">@payment.RateFormatted</td>
|
||||||
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
|
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td class="fw-normal" colspan="5">
|
<td class="fw-normal" colspan="5">
|
||||||
<span class="text-secondary">Transaction Id:</span>
|
<span class="text-secondary">Transaction Id:</span>
|
||||||
@if (!string.IsNullOrEmpty(payment.Link))
|
@if (!string.IsNullOrEmpty(payment.Link))
|
||||||
@@ -326,17 +322,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td>{{invoice.id}}</td>
|
<td>{{invoice.id}}</td>
|
||||||
<td v-text="formatDate(invoice.expiryDate)"></td>
|
<td v-text="formatDate(invoice.expiryDate)"></td>
|
||||||
<td class="text-end">{{invoice.amountFormatted}}</td>
|
<td class="text-end">{{invoice.amountFormatted}}</td>
|
||||||
<td class="text-end"></td>
|
<td class="text-end"></td>
|
||||||
<td class="text-end text-print-default">
|
<td class="text-end text-print-default">
|
||||||
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
|
<span class="badge" :class="`badge-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-if="invoice.payments && invoice.payments.length > 0">
|
<template v-if="invoice.payments && invoice.payments.length > 0">
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<th class="fw-normal text-secondary">Destination</th>
|
<th class="fw-normal text-secondary">Destination</th>
|
||||||
<th class="fw-normal text-secondary">Received</th>
|
<th class="fw-normal text-secondary">Received</th>
|
||||||
<th class="fw-normal text-secondary text-end">Paid</th>
|
<th class="fw-normal text-secondary text-end">Paid</th>
|
||||||
@@ -344,14 +340,14 @@
|
|||||||
<th class="fw-normal text-secondary text-end">Payment</th>
|
<th class="fw-normal text-secondary text-end">Payment</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="payment of invoice.payments">
|
<template v-for="payment of invoice.payments">
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td class="text-break"><code>{{payment.destination}}</code></td>
|
<td class="text-break"><code>{{payment.destination}}</code></td>
|
||||||
<td v-text="formatDate(payment.receivedDate)"></td>
|
<td v-text="formatDate(payment.receivedDate)"></td>
|
||||||
<td class="text-end">{{payment.paidFormatted}}</td>
|
<td class="text-end">{{payment.paidFormatted}}</td>
|
||||||
<td class="text-end">{{payment.rateFormatted}}</td>
|
<td class="text-end">{{payment.rateFormatted}}</td>
|
||||||
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
|
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="table-borderless table-light">
|
<tr class="table-borderless">
|
||||||
<td class="fw-normal" colspan="5">
|
<td class="fw-normal" colspan="5">
|
||||||
<span class="text-secondary">Transaction Id:</span>
|
<span class="text-secondary">Transaction Id:</span>
|
||||||
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
|
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
|
||||||
|
|||||||
@@ -102,30 +102,30 @@ a.unobtrusive-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Badges */
|
/* Badges */
|
||||||
.badge-pending,
|
.badge-new,
|
||||||
.badge-new {
|
.badge-pending {
|
||||||
background: #d4edda;
|
background: #d4edda;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-expired {
|
.badge-expired {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-invalid {
|
.badge-invalid {
|
||||||
background: #c94a47;
|
background: var(--btcpay-danger);
|
||||||
color: #fff;
|
color: var(--btcpay-danger-text);
|
||||||
|
}
|
||||||
|
.badge-unusual {
|
||||||
|
background: var(--btcpay-warning);
|
||||||
|
color: var(--btcpay-warning-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-processing {
|
.badge-processing {
|
||||||
background: #f1c332;
|
background: var(--btcpay-info);
|
||||||
color: #000;
|
color: var(--btcpay-info-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-settled {
|
.badge-settled {
|
||||||
background: #329f80;
|
background: var(--btcpay-success);
|
||||||
color: #fff;
|
color: var(--btcpay-success-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info icons in main headline */
|
/* Info icons in main headline */
|
||||||
|
|||||||
@@ -95,24 +95,19 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
statusClass: function (state) {
|
statusClass: function (state) {
|
||||||
var [, status,, exceptionStatus] = state.match(/(\w*)\s?(\((\w*)\))?/) || [];
|
const [, status,, exceptionStatus] = state.match(/(\w*)\s?(\((\w*)\))?/) || [];
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "Settled":
|
|
||||||
case "Processing":
|
|
||||||
return "success";
|
|
||||||
case "Expired":
|
case "Expired":
|
||||||
switch (exceptionStatus) {
|
switch (exceptionStatus) {
|
||||||
case "paidLate":
|
case "paidLate":
|
||||||
case "paidPartial":
|
case "paidPartial":
|
||||||
case "paidOver":
|
case "paidOver":
|
||||||
return "warning";
|
return "unusual";
|
||||||
default:
|
default:
|
||||||
return "danger";
|
return "expired";
|
||||||
}
|
}
|
||||||
case "Invalid":
|
|
||||||
return "danger";
|
|
||||||
default:
|
default:
|
||||||
return "warning";
|
return status.toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,7 +159,6 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
|||||||
Vue.toasted.success(title, Object.assign({}, toastOptions), { icon });
|
Vue.toasted.success(title, Object.assign({}, toastOptions), { icon });
|
||||||
});
|
});
|
||||||
eventAggregator.$on("info-updated", function (model) {
|
eventAggregator.$on("info-updated", function (model) {
|
||||||
console.warn("UPDATED", self.srvModel, arguments);
|
|
||||||
self.srvModel = model;
|
self.srvModel = model;
|
||||||
});
|
});
|
||||||
eventAggregator.$on("connection-pending", function () {
|
eventAggregator.$on("connection-pending", function () {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ var hubListener = function () {
|
|||||||
connection.on("InvoiceCreated", function (invoiceId) {
|
connection.on("InvoiceCreated", function (invoiceId) {
|
||||||
eventAggregator.$emit("invoice-created", invoiceId);
|
eventAggregator.$emit("invoice-created", invoiceId);
|
||||||
});
|
});
|
||||||
|
connection.on("InvoiceConfirmed", function (invoiceId) {
|
||||||
|
eventAggregator.$emit("invoice-confirmed", invoiceId);
|
||||||
|
});
|
||||||
connection.on("InvoiceError", function (error) {
|
connection.on("InvoiceError", function (error) {
|
||||||
eventAggregator.$emit("invoice-error", error);
|
eventAggregator.$emit("invoice-error", error);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user