Fix: Incorrect calculation for crowdfund and payment request status (#6381)

This commit is contained in:
Nicolas Dorier 2024-11-13 20:59:52 +09:00 committed by GitHub
parent fdbee350b8
commit 12d4803c5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 81 additions and 86 deletions

View File

@ -66,12 +66,10 @@ namespace BTCPayServer.PaymentRequest
{
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
var allSettled = contributions.All(i => i.Value.Settled);
var isPaid = contributions.TotalCurrency >= blob.Amount;
if (isPaid)
if (contributions.Total >= blob.Amount)
{
currentStatus = allSettled
currentStatus = contributions.TotalSettled >= blob.Amount
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
: Client.Models.PaymentRequestData.PaymentRequestStatus.Processing;
}
@ -99,7 +97,7 @@ namespace BTCPayServer.PaymentRequest
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 amountDue = blob.Amount - paymentStats.Total;
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
.FirstOrDefault(entity => entity.Status == InvoiceStatus.New);
@ -107,8 +105,8 @@ namespace BTCPayServer.PaymentRequest
{
Archived = pr.Archived,
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
AmountCollected = paymentStats.TotalCurrency,
AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.TotalCurrency, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
AmountCollected = paymentStats.Total,
AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.Total, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
AmountDue = amountDue,
AmountDueFormatted = _displayFormatter.Currency(amountDue, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),

View File

@ -151,13 +151,10 @@ namespace BTCPayServer.Plugins.Crowdfund
}
var invoices = await AppService.GetInvoicesForApp(_invoiceRepository, appData, lastResetDate);
var completeInvoices = invoices.Where(IsComplete).ToArray();
var pendingInvoices = invoices.Where(IsPending).ToArray();
var paidInvoices = invoices.Where(IsPaid).ToArray();
var pendingPayments = _invoiceRepository.GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
var currentPayments = _invoiceRepository.GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
var currentPayments = _invoiceRepository.GetContributionsByPaymentMethodId(settings.TargetCurrency, invoices, !settings.EnforceTargetAmount);
var paidInvoices = invoices.Where(e => e.Status is InvoiceStatus.Settled or InvoiceStatus.Processing).ToArray();
var perkCount = paidInvoices
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode))
.GroupBy(entity => entity.Metadata.ItemCode)
@ -225,14 +222,14 @@ namespace BTCPayServer.Plugins.Crowdfund
Info = new ViewCrowdfundViewModel.CrowdfundInfo
{
TotalContributors = paidInvoices.Length,
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
ProgressPercentage = (currentPayments.TotalSettled / settings.TargetAmount) * 100,
PendingProgressPercentage = (currentPayments.TotalProcessing / settings.TargetAmount) * 100,
LastUpdated = DateTime.UtcNow,
PaymentStats = GetPaymentStats(currentPayments, pendingPayments),
PaymentStats = GetPaymentStats(currentPayments),
LastResetDate = lastResetDate,
NextResetDate = nextResetDate,
CurrentPendingAmount = pendingPayments.TotalCurrency,
CurrentAmount = currentPayments.TotalCurrency
CurrentPendingAmount = currentPayments.TotalProcessing,
CurrentAmount = currentPayments.TotalSettled
}
};
var httpContext = _httpContextAccessor.HttpContext;
@ -243,29 +240,18 @@ namespace BTCPayServer.Plugins.Crowdfund
return vm;
}
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats, InvoiceStatistics pendingSats)
{
var r = new Dictionary<string, PaymentStat>();
var allStats = stats.Concat(pendingSats);
var total = allStats
.Select(s => s.Value.CurrencyValue).Sum();
foreach (var kv in allStats
.GroupBy(k => k.Key, k => k.Value)
.Select(g => (g.Key,
PaymentCurrency: g.Select(s => s.Currency).First(),
CurrencyValue: g.Sum(s => s.CurrencyValue))))
{
var pmi = PaymentMethodId.Parse(kv.Key);
r.TryAdd(kv.Key, new PaymentStat()
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats)
=> stats.Total is 0.0m ? new Dictionary<string, PaymentStat>() : stats.ToDictionary(kv => kv.Key, kv =>
{
Label = _prettyNameProvider.PrettyName(pmi),
Percent = (kv.CurrencyValue / total) * 100.0m,
// Note that the LNURL will have the same LN
IsLightning = pmi == PaymentTypes.LN.GetPaymentMethodId(kv.PaymentCurrency)
var pmi = PaymentMethodId.Parse(kv.Key);
return new PaymentStat()
{
Label = _prettyNameProvider.PrettyName(pmi),
Percent = (kv.Value.CurrencyValue / stats.Total) * 100.0m,
// Note that the LNURL will have the same LN
IsLightning = pmi == PaymentTypes.LN.GetPaymentMethodId(kv.Value.Currency)
};
});
}
return r;
}
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
{
@ -279,20 +265,5 @@ namespace BTCPayServer.Plugins.Crowdfund
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UICrowdfundController.ViewCrowdfund),
"UICrowdfund", new { appId = app.Id }, _options.Value.RootPath)!);
}
private static bool IsPaid(InvoiceEntity entity)
{
return entity.Status == InvoiceStatus.Settled || entity.Status == InvoiceStatus.Processing;
}
private static bool IsPending(InvoiceEntity entity)
{
return entity.Status != InvoiceStatus.Settled;
}
private static bool IsComplete(InvoiceEntity entity)
{
return entity.Status == InvoiceStatus.Settled;
}
}
}

View File

@ -830,14 +830,6 @@ namespace BTCPayServer.Services.Invoices
Status == InvoiceStatus.Invalid;
}
public bool IsSettled()
{
return
Status == InvoiceStatus.Settled ||
(Status == InvoiceStatus.Expired &&
ExceptionStatus is InvoiceExceptionStatus.PaidLate or InvoiceExceptionStatus.PaidOver);
}
public override string ToString()
{
return Status + ExceptionStatus switch

View File

@ -70,13 +70,13 @@ namespace BTCPayServer.Services.Invoices
public async Task<InvoiceEntity> GetInvoiceFromAddress(PaymentMethodId paymentMethodId, string address)
{
using var db = _applicationDbContextFactory.CreateContext();
var row = (await db.AddressInvoices
.Include(a => a.InvoiceData.Payments)
.Where(a => a.Address == address && a.PaymentMethodId == paymentMethodId.ToString())
.Select(a => a.InvoiceData)
.FirstOrDefaultAsync());
return row is null ? null : ToEntity(row);
}
var row = (await db.AddressInvoices
.Include(a => a.InvoiceData.Payments)
.Where(a => a.Address == address && a.PaymentMethodId == paymentMethodId.ToString())
.Select(a => a.InvoiceData)
.FirstOrDefaultAsync());
return row is null ? null : ToEntity(row);
}
/// <summary>
/// Returns all invoices which either:
@ -820,6 +820,8 @@ retry:
public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
{
decimal totalSettledCurrency = 0.0m;
decimal totalProcessingCurrency = 0.0m;
var contributions = invoices
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
.SelectMany(p =>
@ -828,30 +830,42 @@ retry:
{
GroupKey = p.Currency,
Currency = p.Currency,
CurrencyValue = p.Price,
Settled = p.GetInvoiceState().IsSettled()
CurrencyValue = p.Price
};
contribution.Value = contribution.CurrencyValue;
// For hardcap, we count newly created invoices as part of the contributions
if (!softcap && p.Status == InvoiceStatus.New)
{
totalProcessingCurrency += contribution.CurrencyValue;
return new[] { contribution };
}
var markedSettled = p is
{
Status: InvoiceStatus.Settled,
ExceptionStatus: InvoiceExceptionStatus.Marked
};
// If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete
var payments = p.GetPayments(true);
if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Settled)
if (payments.Count == 0 && markedSettled)
{
totalSettledCurrency += contribution.CurrencyValue;
return new[] { contribution };
contribution.CurrencyValue = 0m;
contribution.Value = 0m;
}
// If an invoice has been marked invalid, remove the contribution
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Invalid)
if (p is
{
Status: InvoiceStatus.Invalid,
ExceptionStatus: InvoiceExceptionStatus.Marked
})
{
contribution.CurrencyValue = 0m;
contribution.Value = 0m;
return new[] { contribution };
}
// Else, we just sum the payments
return payments
@ -863,9 +877,22 @@ retry:
Currency = pay.Currency,
CurrencyValue = pay.InvoicePaidAmount.Net,
Value = pay.PaidAmount.Net,
Divisibility = pay.Divisibility,
Settled = p.GetInvoiceState().IsSettled()
Divisibility = pay.Divisibility
};
// If paid before expiration or marked settled, account the payments...
if (pay.ReceivedTime <= p.ExpirationTime || markedSettled)
{
if (pay.Status is PaymentStatus.Settled)
totalSettledCurrency += pay.InvoicePaidAmount.Net;
else if (pay.Status is PaymentStatus.Processing)
totalProcessingCurrency += pay.InvoicePaidAmount.Net;
}
else
{
paymentMethodContribution.CurrencyValue = 0m;
paymentMethodContribution.Value = 0m;
}
return paymentMethodContribution;
})
.ToArray();
@ -874,12 +901,16 @@ retry:
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution
{
Currency = p.Select(p => p.Currency).First(),
Settled = p.All(v => v.Settled),
Divisibility = p.Max(p => p.Divisibility),
Value = p.Select(v => v.Value).Sum(),
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
});
return new InvoiceStatistics(contributions);
return new InvoiceStatistics(contributions)
{
TotalSettled = totalSettledCurrency,
TotalProcessing = totalProcessingCurrency,
Total = totalSettledCurrency + totalProcessingCurrency
};
}
private string GetGroupKey(PaymentEntity pay)
@ -967,15 +998,18 @@ retry:
{
public InvoiceStatistics(IEnumerable<KeyValuePair<string, Contribution>> collection) : base(collection)
{
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
}
public decimal TotalCurrency { get; }
public decimal TotalSettled { get; set; }
public decimal TotalProcessing { get; set; }
/// <summary>
/// <see cref="TotalSettled"/> + <see cref="TotalProcessing"/>
/// </summary>
public decimal Total { get; set; }
public class Contribution
{
public string Currency { get; set; }
public int Divisibility { get; set; }
public bool Settled { get; set; }
public decimal Value { get; set; }
public decimal CurrencyValue { get; set; }
public string GroupKey { get; set; }