mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 22:25:28 +01:00
Fix live update of crowdfunding, add tests, consider payments as confirmed if invoice is confirmed
This commit is contained in:
parent
0807f3b87b
commit
3bbf4de5d2
8 changed files with 110 additions and 60 deletions
|
@ -23,6 +23,7 @@ using NBitcoin;
|
|||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.UnitTest1;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
|
@ -183,7 +184,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("We create an invoice with a hardcap");
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
@ -191,6 +193,7 @@ namespace BTCPayServer.Tests
|
|||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
|
@ -207,9 +210,10 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal(0m, model.Info.CurrentAmount );
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
|
@ -225,24 +229,32 @@ namespace BTCPayServer.Tests
|
|||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount );
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount);
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal( 0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
|
||||
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
|
||||
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
|
@ -255,6 +267,30 @@ namespace BTCPayServer.Tests
|
|||
}, Facade.Merchant);
|
||||
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
|
||||
crowdfundViewModel.EnforceTargetAmount = false;
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(0.5m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class CrowdfundAppUpdated
|
||||
public class AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public CrowdfundSettings Settings { get; set; }
|
||||
public object Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,6 @@ namespace BTCPayServer.Controllers
|
|||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
UseInvoiceAmount = settings.UseInvoiceAmount,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = app.TagAllInvoices,
|
||||
|
@ -120,7 +119,6 @@ namespace BTCPayServer.Controllers
|
|||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
UseInvoiceAmount = vm.UseInvoiceAmount,
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
};
|
||||
|
@ -129,7 +127,7 @@ namespace BTCPayServer.Controllers
|
|||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
|
||||
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
||||
_EventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
|
|
|
@ -106,11 +106,11 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _AppService.GetCrowdfundInfo(appId));
|
||||
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _AppService.GetCrowdfundInfo(appId));
|
||||
return View(await _AppService.GetAppInfo(appId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers
|
|||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = await _AppService.GetCrowdfundInfo(appId);
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
|
||||
if (!isAdmin &&
|
||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||
|
|
|
@ -69,8 +69,6 @@ namespace BTCPayServer.Models.AppViewModels
|
|||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
|
||||
[Display(Name = "Base the contributed goal amount on the invoice amount and not actual payments")]
|
||||
public bool UseInvoiceAmount { get; set; }
|
||||
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
|
||||
|
|
|
@ -27,15 +27,17 @@ namespace BTCPayServer.Services.Apps
|
|||
{
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IHubContext<AppHub> _HubContext;
|
||||
|
||||
private readonly AppService _appService;
|
||||
private List<IEventAggregatorSubscription> _Subscriptions;
|
||||
private CancellationTokenSource _Cts;
|
||||
|
||||
public AppHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<AppHub> hubContext)
|
||||
IHubContext<AppHub> hubContext,
|
||||
AppService appService)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
private async Task NotifyClients(string appId, InvoiceEvent invoiceEvent, CancellationToken cancellationToken)
|
||||
|
@ -51,19 +53,27 @@ namespace BTCPayServer.Services.Apps
|
|||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
}, cancellationToken);
|
||||
}
|
||||
await InfoUpdated(appId);
|
||||
}
|
||||
|
||||
Channel<InvoiceEvent> _InvoiceEvents = Channel.CreateUnbounded<InvoiceEvent>();
|
||||
Channel<object> _Events = Channel.CreateUnbounded<object>();
|
||||
public async Task ProcessEvents(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _InvoiceEvents.Reader.WaitToReadAsync(cancellationToken))
|
||||
while (await _Events.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
if (_InvoiceEvents.Reader.TryRead(out var evt))
|
||||
if (_Events.Reader.TryRead(out var evt))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach(var appId in AppService.GetAppInternalTags(evt.Invoice.InternalTags))
|
||||
await NotifyClients(appId, evt, cancellationToken);
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice.InternalTags))
|
||||
await NotifyClients(appId, invoiceEvent, cancellationToken);
|
||||
}
|
||||
else if (evt is AppsController.AppUpdated app)
|
||||
{
|
||||
await InfoUpdated(app.AppId);
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
@ -77,11 +87,18 @@ namespace BTCPayServer.Services.Apps
|
|||
}
|
||||
}
|
||||
|
||||
private async Task InfoUpdated(string appId)
|
||||
{
|
||||
var info = await _appService.GetAppInfo(appId);
|
||||
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.InfoUpdated, new object[] { info });
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions = new List<IEventAggregatorSubscription>()
|
||||
{
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(e => _InvoiceEvents.Writer.TryWrite(e))
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(e => _Events.Writer.TryWrite(e)),
|
||||
_EventAggregator.Subscribe<AppsController.AppUpdated>(e => _Events.Writer.TryWrite(e))
|
||||
};
|
||||
_Cts = new CancellationTokenSource();
|
||||
_ProcessingEvents = ProcessEvents(_Cts.Token);
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace BTCPayServer.Services.Apps
|
|||
_Networks = networks;
|
||||
}
|
||||
|
||||
public async Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
public async Task<object> GetAppInfo(string appId)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.Crowdfund, true);
|
||||
return await GetInfo(app);
|
||||
|
@ -91,13 +91,13 @@ namespace BTCPayServer.Services.Apps
|
|||
}
|
||||
|
||||
var invoices = await GetInvoicesForApp(appData, lastResetDate);
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
paymentStats,
|
||||
|
@ -106,9 +106,6 @@ namespace BTCPayServer.Services.Apps
|
|||
pendingPaymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
|
||||
|
||||
|
||||
|
||||
var perkCount = invoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
|
||||
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
||||
|
@ -325,25 +322,35 @@ namespace BTCPayServer.Services.Apps
|
|||
return result.Sum();
|
||||
}
|
||||
|
||||
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool usePaymentData = true)
|
||||
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
if (usePaymentData)
|
||||
{
|
||||
var payments = invoices.SelectMany(entity => entity.GetPayments());
|
||||
return invoices
|
||||
.SelectMany(p =>
|
||||
{
|
||||
// For hardcap, we count newly created invoices as part of the contributions
|
||||
if (!softcap && p.Status == InvoiceStatus.New)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
|
||||
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
|
||||
// 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();
|
||||
if (payments.Count == 0 &&
|
||||
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Complete)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
|
||||
|
||||
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
|
||||
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return invoices
|
||||
.GroupBy(entity => entity.ProductInformation.Currency)
|
||||
.ToDictionary(
|
||||
entities => entities.Key,
|
||||
entities => entities.Sum(entity => entity.ProductInformation.Price));
|
||||
}
|
||||
// If an invoice has been marked invalid, remove the contribution
|
||||
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
|
||||
p.Status == InvoiceStatus.Invalid)
|
||||
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
|
||||
|
||||
// Else, we just sum the payments
|
||||
return payments
|
||||
.GroupBy(pay => pay.GetPaymentMethodId())
|
||||
.Select(payGroup => (Key: payGroup.Key.ToString(),
|
||||
Value: payGroup.Select(pay => pay.GetCryptoPaymentData().GetValue()).Sum())).ToArray();
|
||||
})
|
||||
.ToDictionary(p => p.Key, p => p.Value);
|
||||
}
|
||||
|
||||
private class PosHolder
|
||||
|
|
|
@ -28,7 +28,6 @@ namespace BTCPayServer.Services.Apps
|
|||
public bool SoundsEnabled { get; set; } = true;
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
public bool UseInvoiceAmount { get; set; } = true;
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||
[Obsolete("Use AppData.TagAllInvoices instead")]
|
||||
|
|
|
@ -157,11 +157,6 @@
|
|||
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UseInvoiceAmount"></label>
|
||||
<input asp-for="UseInvoiceAmount" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="UseInvoiceAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UseAllStoreInvoices"></label>
|
||||
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue