mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Refactor Crowdfund to use the tagging system
This commit is contained in:
@@ -218,7 +218,6 @@ namespace BTCPayServer.Tests
|
|||||||
Price = 1m,
|
Price = 1m,
|
||||||
Currency = "BTC",
|
Currency = "BTC",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
|
||||||
ItemDesc = "Some description",
|
ItemDesc = "Some description",
|
||||||
TransactionSpeed = "high",
|
TransactionSpeed = "high",
|
||||||
FullNotifications = true
|
FullNotifications = true
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ namespace BTCPayServer.Controllers
|
|||||||
public bool UseInvoiceAmount { get; set; } = true;
|
public bool UseInvoiceAmount { get; set; } = true;
|
||||||
public int ResetEveryAmount { get; set; } = 1;
|
public int ResetEveryAmount { get; set; } = 1;
|
||||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||||
|
[Obsolete("Use AppData.TagAllInvoices instead")]
|
||||||
public bool UseAllStoreInvoices { get; set; }
|
public bool UseAllStoreInvoices { get; set; }
|
||||||
public bool DisplayPerksRanking { get; set; }
|
public bool DisplayPerksRanking { get; set; }
|
||||||
public bool SortPerksByPopularity { get; set; }
|
public bool SortPerksByPopularity { get; set; }
|
||||||
@@ -80,9 +81,9 @@ namespace BTCPayServer.Controllers
|
|||||||
UseInvoiceAmount = settings.UseInvoiceAmount,
|
UseInvoiceAmount = settings.UseInvoiceAmount,
|
||||||
ResetEveryAmount = settings.ResetEveryAmount,
|
ResetEveryAmount = settings.ResetEveryAmount,
|
||||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||||
UseAllStoreInvoices = settings.UseAllStoreInvoices,
|
UseAllStoreInvoices = app.TagAllInvoices,
|
||||||
AppId = appId,
|
AppId = appId,
|
||||||
SearchTerm = settings.UseAllStoreInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppsHelper.GetCrowdfundOrderId(appId)}",
|
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppsHelper.GetCrowdfundOrderId(appId)}",
|
||||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||||
};
|
};
|
||||||
@@ -152,13 +153,14 @@ namespace BTCPayServer.Controllers
|
|||||||
ResetEveryAmount = vm.ResetEveryAmount,
|
ResetEveryAmount = vm.ResetEveryAmount,
|
||||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||||
UseInvoiceAmount = vm.UseInvoiceAmount,
|
UseInvoiceAmount = vm.UseInvoiceAmount,
|
||||||
UseAllStoreInvoices = vm.UseAllStoreInvoices,
|
|
||||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||||
app.SetSettings(newSettings);
|
app.SetSettings(newSettings);
|
||||||
await UpdateAppSettings(app);
|
await UpdateAppSettings(app);
|
||||||
|
|
||||||
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
||||||
{
|
{
|
||||||
AppId = appId,
|
AppId = appId,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ctx.Apps.Add(app);
|
ctx.Apps.Add(app);
|
||||||
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
||||||
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
|
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
|
||||||
|
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,11 @@ namespace BTCPayServer.Controllers
|
|||||||
public class AppsPublicController : Controller
|
public class AppsPublicController : Controller
|
||||||
{
|
{
|
||||||
public AppsPublicController(AppsHelper appsHelper,
|
public AppsPublicController(AppsHelper appsHelper,
|
||||||
InvoiceController invoiceController,
|
InvoiceController invoiceController,
|
||||||
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
|
UserManager<ApplicationUser> userManager)
|
||||||
{
|
{
|
||||||
_AppsHelper = appsHelper;
|
_AppsHelper = appsHelper;
|
||||||
_InvoiceController = invoiceController;
|
_InvoiceController = invoiceController;
|
||||||
_CrowdfundHubStreamer = crowdfundHubStreamer;
|
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +108,11 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||||
}
|
}
|
||||||
if (settings.Enabled) return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
if (settings.Enabled) return View(await _AppsHelper.GetCrowdfundInfo(appId));
|
||||||
if(!isAdmin)
|
if(!isAdmin)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
return View(await _AppsHelper.GetCrowdfundInfo(appId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -138,7 +137,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound("Crowdfund is not currently active");
|
return NotFound("Crowdfund is not currently active");
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = await _CrowdfundHubStreamer.GetCrowdfundInfo(appId);
|
var info = await _AppsHelper.GetCrowdfundInfo(appId);
|
||||||
|
|
||||||
if (!isAdmin &&
|
if (!isAdmin &&
|
||||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||||
@@ -177,7 +176,6 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
|
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
|
||||||
{
|
{
|
||||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
|
||||||
Currency = settings.TargetCurrency,
|
Currency = settings.TargetCurrency,
|
||||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||||
ItemDesc = title,
|
ItemDesc = title,
|
||||||
@@ -187,7 +185,7 @@ namespace BTCPayServer.Controllers
|
|||||||
FullNotifications = true,
|
FullNotifications = true,
|
||||||
ExtendedNotifications = true,
|
ExtendedNotifications = true,
|
||||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppsHelper.GetAppInternalTag(appId) });
|
||||||
if (request.RedirectToCheckout)
|
if (request.RedirectToCheckout)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||||
@@ -204,7 +202,6 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("/apps/{appId}/pos")]
|
[Route("/apps/{appId}/pos")]
|
||||||
@@ -281,16 +278,169 @@ namespace BTCPayServer.Controllers
|
|||||||
public class AppsHelper
|
public class AppsHelper
|
||||||
{
|
{
|
||||||
ApplicationDbContextFactory _ContextFactory;
|
ApplicationDbContextFactory _ContextFactory;
|
||||||
|
private readonly InvoiceRepository _InvoiceRepository;
|
||||||
CurrencyNameTable _Currencies;
|
CurrencyNameTable _Currencies;
|
||||||
private readonly RateFetcher _RateFetcher;
|
private readonly RateFetcher _RateFetcher;
|
||||||
private readonly HtmlSanitizer _HtmlSanitizer;
|
private readonly HtmlSanitizer _HtmlSanitizer;
|
||||||
|
private readonly BTCPayNetworkProvider _Networks;
|
||||||
public CurrencyNameTable Currencies => _Currencies;
|
public CurrencyNameTable Currencies => _Currencies;
|
||||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies, RateFetcher rateFetcher, HtmlSanitizer htmlSanitizer)
|
public AppsHelper(ApplicationDbContextFactory contextFactory,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
|
BTCPayNetworkProvider networks,
|
||||||
|
CurrencyNameTable currencies,
|
||||||
|
RateFetcher rateFetcher,
|
||||||
|
HtmlSanitizer htmlSanitizer)
|
||||||
{
|
{
|
||||||
_ContextFactory = contextFactory;
|
_ContextFactory = contextFactory;
|
||||||
|
_InvoiceRepository = invoiceRepository;
|
||||||
_Currencies = currencies;
|
_Currencies = currencies;
|
||||||
_RateFetcher = rateFetcher;
|
_RateFetcher = rateFetcher;
|
||||||
_HtmlSanitizer = htmlSanitizer;
|
_HtmlSanitizer = htmlSanitizer;
|
||||||
|
_Networks = networks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||||
|
{
|
||||||
|
var app = await GetApp(appId, AppType.Crowdfund, true);
|
||||||
|
return await GetInfo(app);
|
||||||
|
}
|
||||||
|
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage = null)
|
||||||
|
{
|
||||||
|
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||||
|
var resetEvery = settings.StartDate.HasValue ? settings.ResetEvery : CrowdfundResetEvery.Never;
|
||||||
|
DateTime? lastResetDate = null;
|
||||||
|
DateTime? nextResetDate = null;
|
||||||
|
if (resetEvery != CrowdfundResetEvery.Never)
|
||||||
|
{
|
||||||
|
lastResetDate = settings.StartDate.Value;
|
||||||
|
|
||||||
|
nextResetDate = lastResetDate.Value;
|
||||||
|
while (DateTime.Now >= nextResetDate)
|
||||||
|
{
|
||||||
|
lastResetDate = nextResetDate;
|
||||||
|
switch (resetEvery)
|
||||||
|
{
|
||||||
|
case CrowdfundResetEvery.Hour:
|
||||||
|
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Day:
|
||||||
|
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Month:
|
||||||
|
|
||||||
|
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
case CrowdfundResetEvery.Year:
|
||||||
|
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
|
||||||
|
|
||||||
|
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
|
||||||
|
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
|
||||||
|
|
||||||
|
var currentAmount = await GetCurrentContributionAmount(
|
||||||
|
paymentStats,
|
||||||
|
settings.TargetCurrency, rateRules);
|
||||||
|
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||||
|
pendingPaymentStats,
|
||||||
|
settings.TargetCurrency, rateRules);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var perkCount = invoices
|
||||||
|
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
|
||||||
|
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
||||||
|
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||||
|
|
||||||
|
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||||
|
if (settings.SortPerksByPopularity)
|
||||||
|
{
|
||||||
|
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
||||||
|
var newPerksOrder = ordered
|
||||||
|
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
||||||
|
.Where(matchingPerk => matchingPerk != null)
|
||||||
|
.ToList();
|
||||||
|
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
||||||
|
newPerksOrder.AddRange(remainingPerks);
|
||||||
|
perks = newPerksOrder.ToArray();
|
||||||
|
}
|
||||||
|
return new ViewCrowdfundViewModel()
|
||||||
|
{
|
||||||
|
Title = settings.Title,
|
||||||
|
Tagline = settings.Tagline,
|
||||||
|
Description = settings.Description,
|
||||||
|
CustomCSSLink = settings.CustomCSSLink,
|
||||||
|
MainImageUrl = settings.MainImageUrl,
|
||||||
|
EmbeddedCSS = settings.EmbeddedCSS,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
AppId = appData.Id,
|
||||||
|
StartDate = settings.StartDate?.ToUniversalTime(),
|
||||||
|
EndDate = settings.EndDate?.ToUniversalTime(),
|
||||||
|
TargetAmount = settings.TargetAmount,
|
||||||
|
TargetCurrency = settings.TargetCurrency,
|
||||||
|
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||||
|
StatusMessage = statusMessage,
|
||||||
|
Perks = perks,
|
||||||
|
DisqusEnabled = settings.DisqusEnabled,
|
||||||
|
SoundsEnabled = settings.SoundsEnabled,
|
||||||
|
DisqusShortname = settings.DisqusShortname,
|
||||||
|
AnimationsEnabled = settings.AnimationsEnabled,
|
||||||
|
ResetEveryAmount = settings.ResetEveryAmount,
|
||||||
|
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||||
|
PerkCount = perkCount,
|
||||||
|
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||||
|
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||||
|
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||||
|
{
|
||||||
|
TotalContributors = invoices.Length,
|
||||||
|
CurrentPendingAmount = currentPendingAmount,
|
||||||
|
CurrentAmount = currentAmount,
|
||||||
|
ProgressPercentage = (currentAmount / settings.TargetAmount) * 100,
|
||||||
|
PendingProgressPercentage = (currentPendingAmount / settings.TargetAmount) * 100,
|
||||||
|
LastUpdated = DateTime.Now,
|
||||||
|
PaymentStats = paymentStats,
|
||||||
|
PendingPaymentStats = pendingPaymentStats,
|
||||||
|
LastResetDate = lastResetDate,
|
||||||
|
NextResetDate = nextResetDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
||||||
|
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||||
|
public static string[] GetAppInternalTags(IEnumerable<string> tags)
|
||||||
|
{
|
||||||
|
return tags == null ? Array.Empty<string>() : tags
|
||||||
|
.Where(t => t.StartsWith("APP#", StringComparison.InvariantCulture))
|
||||||
|
.Select(t => t.Substring("APP#".Length)).ToArray();
|
||||||
|
}
|
||||||
|
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
||||||
|
{
|
||||||
|
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||||
|
{
|
||||||
|
StoreId = new[] { appData.StoreData.Id },
|
||||||
|
OrderId = appData.TagAllInvoices ? null : new[] { GetCrowdfundOrderId(appData.Id) },
|
||||||
|
Status = new string[]{
|
||||||
|
InvoiceState.ToString(InvoiceStatus.New),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||||
|
InvoiceState.ToString(InvoiceStatus.Complete)},
|
||||||
|
StartDate = startDate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Old invoices may have invoices which were not tagged
|
||||||
|
invoices = invoices.Where(inv => inv.Version < InvoiceEntity.InternalTagSupport_Version ||
|
||||||
|
inv.InternalTags.Contains(GetAppInternalTag(appData.Id))).ToArray();
|
||||||
|
return invoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoreData[]> GetOwnedStores(string userId)
|
public async Task<StoreData[]> GetOwnedStores(string userId)
|
||||||
@@ -484,6 +634,5 @@ namespace BTCPayServer.Controllers
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,11 +189,6 @@ namespace BTCPayServer.Controllers
|
|||||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Task CreateInvoiceCore(Invoice invoice, StoreData store, string v1, string[] v2)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||||
{
|
{
|
||||||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ using System;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Hubs;
|
using BTCPayServer.Hubs;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
@@ -15,304 +18,88 @@ using BTCPayServer.Services.Invoices;
|
|||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Crowdfund
|
namespace BTCPayServer.Crowdfund
|
||||||
{
|
{
|
||||||
public class CrowdfundHubStreamer: IDisposable
|
public class CrowdfundHubStreamer : IHostedService
|
||||||
{
|
{
|
||||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
|
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
private readonly IHubContext<CrowdfundHub> _HubContext;
|
private readonly IHubContext<CrowdfundHub> _HubContext;
|
||||||
private readonly IMemoryCache _MemoryCache;
|
|
||||||
private readonly AppsHelper _AppsHelper;
|
|
||||||
private readonly RateFetcher _RateFetcher;
|
|
||||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
|
||||||
private readonly InvoiceRepository _InvoiceRepository;
|
|
||||||
private readonly CurrencyNameTable _currencies;
|
|
||||||
private readonly ILogger<CrowdfundHubStreamer> _Logger;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
|
|
||||||
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
|
|
||||||
|
|
||||||
private List<IEventAggregatorSubscription> _Subscriptions;
|
private List<IEventAggregatorSubscription> _Subscriptions;
|
||||||
|
private CancellationTokenSource _Cts;
|
||||||
|
|
||||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||||
IHubContext<CrowdfundHub> hubContext,
|
IHubContext<CrowdfundHub> hubContext)
|
||||||
IMemoryCache memoryCache,
|
|
||||||
AppsHelper appsHelper,
|
|
||||||
RateFetcher rateFetcher,
|
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
|
||||||
InvoiceRepository invoiceRepository,
|
|
||||||
CurrencyNameTable currencies,
|
|
||||||
ILogger<CrowdfundHubStreamer> logger)
|
|
||||||
{
|
{
|
||||||
_EventAggregator = eventAggregator;
|
_EventAggregator = eventAggregator;
|
||||||
_HubContext = hubContext;
|
_HubContext = hubContext;
|
||||||
_MemoryCache = memoryCache;
|
|
||||||
_AppsHelper = appsHelper;
|
|
||||||
_RateFetcher = rateFetcher;
|
|
||||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
|
||||||
_InvoiceRepository = invoiceRepository;
|
|
||||||
_currencies = currencies;
|
|
||||||
_Logger = logger;
|
|
||||||
#pragma warning disable 4014
|
|
||||||
InitLookup();
|
|
||||||
#pragma warning restore 4014
|
|
||||||
SubscribeToEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitLookup()
|
private async Task NotifyClients(string appId, InvoiceEvent invoiceEvent, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var apps = await _AppsHelper.GetAllApps(null, true);
|
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||||
apps = apps.Where(model => Enum.Parse<AppType>(model.AppType) == AppType.Crowdfund).ToArray();
|
{
|
||||||
var tasks = new List<Task>();
|
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||||
tasks.AddRange(apps.Select(app => Task.Run(async () =>
|
await _HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||||
{
|
{
|
||||||
var fullApp = await _AppsHelper.GetApp(app.Id, AppType.Crowdfund, false);
|
data.GetValue(),
|
||||||
var settings = fullApp.GetSettings<AppsController.CrowdfundSettings>();
|
invoiceEvent.Payment.GetCryptoCode(),
|
||||||
UpdateLookup(app.Id, app.StoreId, settings);
|
Enum.GetName(typeof(PaymentTypes),
|
||||||
})));
|
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||||
await Task.WhenAll(tasks);
|
}, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateLookup(string appId, string storeId, AppsController.CrowdfundSettings settings)
|
Channel<InvoiceEvent> _InvoiceEvents = Channel.CreateUnbounded<InvoiceEvent>();
|
||||||
|
public async Task ProcessEvents(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_QuickAppInvoiceLookup.AddOrReplace(storeId,
|
while (await _InvoiceEvents.Reader.WaitToReadAsync(cancellationToken))
|
||||||
(
|
{
|
||||||
appId: appId,
|
if (_InvoiceEvents.Reader.TryRead(out var evt))
|
||||||
useAllStoreInvoices: settings?.UseAllStoreInvoices ?? false,
|
{
|
||||||
useInvoiceAmount: settings?.UseInvoiceAmount ?? false
|
try
|
||||||
));
|
{
|
||||||
|
foreach(var appId in AppsHelper.GetAppInternalTags(evt.Invoice.InternalTags))
|
||||||
|
await NotifyClients(appId, evt, cancellationToken);
|
||||||
|
}
|
||||||
|
catch when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logs.PayServer.LogWarning(ex, "Unhandled exception in CrowdfundHubStream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
return _MemoryCache.GetOrCreateAsync(GetCacheKey(appId), async entry =>
|
|
||||||
{
|
|
||||||
_Logger.LogInformation($"GetCrowdfundInfo {appId}");
|
|
||||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
|
||||||
var result = await GetInfo(app);
|
|
||||||
entry.SetValue(result);
|
|
||||||
|
|
||||||
TimeSpan? expire = null;
|
|
||||||
|
|
||||||
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
|
|
||||||
{
|
|
||||||
expire = result.StartDate.Value.Subtract(DateTime.Now);
|
|
||||||
}
|
|
||||||
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
|
|
||||||
{
|
|
||||||
expire = result.EndDate.Value.Subtract(DateTime.Now);
|
|
||||||
}
|
|
||||||
if(!expire.HasValue || expire?.TotalMinutes > 5 || expire?.TotalMilliseconds <= 0)
|
|
||||||
{
|
|
||||||
expire = TimeSpan.FromMinutes(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.AbsoluteExpirationRelativeToNow = expire;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubscribeToEvents()
|
|
||||||
{
|
{
|
||||||
_Subscriptions = new List<IEventAggregatorSubscription>()
|
_Subscriptions = new List<IEventAggregatorSubscription>()
|
||||||
{
|
{
|
||||||
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent),
|
_EventAggregator.Subscribe<InvoiceEvent>(e => _InvoiceEvents.Writer.TryWrite(e))
|
||||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
|
||||||
{
|
|
||||||
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
|
|
||||||
InvalidateCacheForApp(updated.AppId);
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
_Cts = new CancellationTokenSource();
|
||||||
|
_ProcessingEvents = ProcessEvents(_Cts.Token);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
Task _ProcessingEvents = Task.CompletedTask;
|
||||||
|
|
||||||
private string GetCacheKey(string appId)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
||||||
}
|
_Cts?.Cancel();
|
||||||
|
try
|
||||||
private void OnInvoiceEvent(InvoiceEvent invoiceEvent)
|
|
||||||
{
|
|
||||||
if (!_QuickAppInvoiceLookup.TryGetValue(invoiceEvent.Invoice.StoreId, out var quickLookup) ||
|
|
||||||
(!quickLookup.useAllStoreInvoices &&
|
|
||||||
!string.IsNullOrEmpty(invoiceEvent.Invoice.OrderId) &&
|
|
||||||
!invoiceEvent.Invoice.OrderId.Equals($"{CrowdfundInvoiceOrderIdPrefix}{quickLookup.appId}", StringComparison.InvariantCulture)
|
|
||||||
))
|
|
||||||
{
|
{
|
||||||
return;
|
await _ProcessingEvents;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
switch (invoiceEvent.Name)
|
{ }
|
||||||
{
|
|
||||||
case InvoiceEvent.ReceivedPayment:
|
|
||||||
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
|
||||||
_HubContext.Clients.Group(quickLookup.appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
|
||||||
{
|
|
||||||
data.GetValue(),
|
|
||||||
invoiceEvent.Payment.GetCryptoCode(),
|
|
||||||
Enum.GetName(typeof(PaymentTypes),
|
|
||||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
|
||||||
} );
|
|
||||||
_Logger.LogInformation($"App {quickLookup.appId}: Received Payment");
|
|
||||||
InvalidateCacheForApp(quickLookup.appId);
|
|
||||||
break;
|
|
||||||
case InvoiceEvent.Created:
|
|
||||||
case InvoiceEvent.MarkedInvalid:
|
|
||||||
case InvoiceEvent.MarkedCompleted:
|
|
||||||
if (quickLookup.useInvoiceAmount)
|
|
||||||
{
|
|
||||||
InvalidateCacheForApp(quickLookup.appId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case InvoiceEvent.Completed:
|
|
||||||
InvalidateCacheForApp(quickLookup.appId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InvalidateCacheForApp(string appId)
|
|
||||||
{
|
|
||||||
_Logger.LogInformation($"App {appId} cache invalidated");
|
|
||||||
_MemoryCache.Remove(GetCacheKey(appId));
|
|
||||||
|
|
||||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
|
||||||
{
|
|
||||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
|
||||||
}, TaskScheduler.Current);
|
|
||||||
|
|
||||||
}
|
|
||||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
|
||||||
{
|
|
||||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
|
||||||
|
|
||||||
var resetEvery = settings.StartDate.HasValue? settings.ResetEvery : CrowdfundResetEvery.Never;
|
|
||||||
DateTime? lastResetDate = null;
|
|
||||||
DateTime? nextResetDate = null;
|
|
||||||
if (resetEvery != CrowdfundResetEvery.Never)
|
|
||||||
{
|
|
||||||
lastResetDate = settings.StartDate.Value;
|
|
||||||
|
|
||||||
nextResetDate = lastResetDate.Value;
|
|
||||||
while (DateTime.Now >= nextResetDate)
|
|
||||||
{
|
|
||||||
lastResetDate = nextResetDate;
|
|
||||||
switch (resetEvery)
|
|
||||||
{
|
|
||||||
case CrowdfundResetEvery.Hour:
|
|
||||||
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Day:
|
|
||||||
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Month:
|
|
||||||
|
|
||||||
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
case CrowdfundResetEvery.Year:
|
|
||||||
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoices = await GetInvoicesForApp(settings.UseAllStoreInvoices? null : appData.Id, lastResetDate);
|
|
||||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
|
|
||||||
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
|
|
||||||
|
|
||||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
|
||||||
|
|
||||||
var pendingPaymentStats = _AppsHelper.GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
|
|
||||||
var paymentStats = _AppsHelper.GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
|
|
||||||
|
|
||||||
var currentAmount = await _AppsHelper.GetCurrentContributionAmount(
|
|
||||||
paymentStats,
|
|
||||||
settings.TargetCurrency, rateRules);
|
|
||||||
var currentPendingAmount = await _AppsHelper.GetCurrentContributionAmount(
|
|
||||||
pendingPaymentStats,
|
|
||||||
settings.TargetCurrency, rateRules);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var perkCount = invoices
|
|
||||||
.Where(entity => !string.IsNullOrEmpty( entity.ProductInformation.ItemCode))
|
|
||||||
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
|
||||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
|
||||||
|
|
||||||
var perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
|
||||||
if (settings.SortPerksByPopularity)
|
|
||||||
{
|
|
||||||
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
|
||||||
var newPerksOrder = ordered
|
|
||||||
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
|
||||||
.Where(matchingPerk => matchingPerk != null)
|
|
||||||
.ToList();
|
|
||||||
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
|
||||||
newPerksOrder.AddRange(remainingPerks);
|
|
||||||
perks = newPerksOrder.ToArray();
|
|
||||||
}
|
|
||||||
return new ViewCrowdfundViewModel()
|
|
||||||
{
|
|
||||||
Title = settings.Title,
|
|
||||||
Tagline = settings.Tagline,
|
|
||||||
Description = settings.Description,
|
|
||||||
CustomCSSLink = settings.CustomCSSLink,
|
|
||||||
MainImageUrl = settings.MainImageUrl,
|
|
||||||
EmbeddedCSS = settings.EmbeddedCSS,
|
|
||||||
StoreId = appData.StoreDataId,
|
|
||||||
AppId = appData.Id,
|
|
||||||
StartDate = settings.StartDate?.ToUniversalTime(),
|
|
||||||
EndDate = settings.EndDate?.ToUniversalTime(),
|
|
||||||
TargetAmount = settings.TargetAmount,
|
|
||||||
TargetCurrency = settings.TargetCurrency,
|
|
||||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
|
||||||
StatusMessage = statusMessage,
|
|
||||||
Perks = perks,
|
|
||||||
DisqusEnabled = settings.DisqusEnabled,
|
|
||||||
SoundsEnabled = settings.SoundsEnabled,
|
|
||||||
DisqusShortname = settings.DisqusShortname,
|
|
||||||
AnimationsEnabled = settings.AnimationsEnabled,
|
|
||||||
ResetEveryAmount = settings.ResetEveryAmount,
|
|
||||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
|
||||||
PerkCount = perkCount,
|
|
||||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery),settings.ResetEvery),
|
|
||||||
CurrencyData = _currencies.GetCurrencyData(settings.TargetCurrency, true),
|
|
||||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
|
||||||
{
|
|
||||||
TotalContributors = invoices.Length,
|
|
||||||
CurrentPendingAmount = currentPendingAmount,
|
|
||||||
CurrentAmount = currentAmount,
|
|
||||||
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
|
||||||
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
|
||||||
LastUpdated = DateTime.Now,
|
|
||||||
PaymentStats = paymentStats,
|
|
||||||
PendingPaymentStats = pendingPaymentStats,
|
|
||||||
LastResetDate = lastResetDate,
|
|
||||||
NextResetDate = nextResetDate
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(string appId, DateTime? startDate = null)
|
|
||||||
{
|
|
||||||
return await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
|
||||||
{
|
|
||||||
OrderId = appId == null? null : new []{$"{CrowdfundInvoiceOrderIdPrefix}{appId}"},
|
|
||||||
Status = new string[]{
|
|
||||||
InvoiceState.ToString(InvoiceStatus.New),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
|
||||||
InvoiceState.ToString(InvoiceStatus.Complete)},
|
|
||||||
StartDate = startDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_Subscriptions.ForEach(subscription => subscription.Dispose());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<TokenRepository>();
|
services.TryAddSingleton<TokenRepository>();
|
||||||
services.TryAddSingleton<EventAggregator>();
|
services.TryAddSingleton<EventAggregator>();
|
||||||
services.TryAddSingleton<CoinAverageSettings>();
|
services.TryAddSingleton<CoinAverageSettings>();
|
||||||
services.TryAddSingleton<CrowdfundHubStreamer>();
|
|
||||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||||
{
|
{
|
||||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||||
@@ -185,6 +184,8 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||||
|
services.AddSingleton<IHostedService, CrowdfundHubStreamer>();
|
||||||
|
|
||||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public bool UseAllStoreInvoices { get; set; }
|
public bool UseAllStoreInvoices { get; set; }
|
||||||
|
|
||||||
public string AppId { get; set; }
|
public string AppId { get; set; }
|
||||||
|
public string SearchTerm { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Sort contribution perks by popularity")]
|
[Display(Name = "Sort contribution perks by popularity")]
|
||||||
public bool SortPerksByPopularity { get; set; }
|
public bool SortPerksByPopularity { get; set; }
|
||||||
[Display(Name = "Display contribution ranking")]
|
[Display(Name = "Display contribution ranking")]
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
}
|
}
|
||||||
public class InvoiceEntity
|
public class InvoiceEntity
|
||||||
{
|
{
|
||||||
|
public const int InternalTagSupport_Version = 1;
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
public string Id
|
public string Id
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ retry:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AppData[]> GetAppsTaggingStore(string storeId)
|
||||||
|
{
|
||||||
|
if (storeId == null)
|
||||||
|
throw new ArgumentNullException(nameof(storeId));
|
||||||
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
|
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
|
||||||
{
|
{
|
||||||
using (var ctx = _ContextFactory.CreateContext())
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
|||||||
Reference in New Issue
Block a user