mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
add in more info and simplify backend model
This commit is contained in:
parent
2aa097be46
commit
5a3f7b5b70
8 changed files with 428 additions and 359 deletions
|
@ -1,22 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
|
@ -52,232 +39,4 @@ namespace BTCPayServer.Hubs
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
public class CrowdfundHubStreamer
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app:";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
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 Dictionary<string, CancellationTokenSource> _CacheTokens = new Dictionary<string, CancellationTokenSource>();
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
AppsHelper appsHelper,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_MemoryCache = memoryCache;
|
||||
_AppsHelper = appsHelper;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
{
|
||||
var key = GetCacheKey(appId);
|
||||
return _MemoryCache.GetOrCreateAsync(key, async entry =>
|
||||
{
|
||||
if (_CacheTokens.ContainsKey(key))
|
||||
{
|
||||
_CacheTokens.Remove(key);
|
||||
}
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
var result = await GetInfo(app);
|
||||
entry.SetValue(result);
|
||||
|
||||
var token = new CancellationTokenSource();
|
||||
_CacheTokens.Add(key, token);
|
||||
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
|
||||
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 = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = expire;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
{
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
{
|
||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||
}
|
||||
|
||||
private void Subscription(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
if (!invoiceEvent.Invoice.OrderId.StartsWith(CrowdfundInvoiceOrderIdPrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var appId = invoiceEvent.Invoice.OrderId.Replace(CrowdfundInvoiceOrderIdPrefix, "", StringComparison.InvariantCultureIgnoreCase);
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
case InvoiceEvent.ReceivedPayment:
|
||||
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||
{
|
||||
invoiceEvent.Payment.GetCryptoPaymentData().GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
} );
|
||||
|
||||
InvalidateCacheForApp(appId);
|
||||
break;
|
||||
case InvoiceEvent.Completed:
|
||||
InvalidateCacheForApp(appId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCacheForApp(string appId)
|
||||
{
|
||||
if (_CacheTokens.ContainsKey(appId))
|
||||
{
|
||||
_CacheTokens[appId].Cancel();
|
||||
}
|
||||
|
||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
||||
{
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
||||
}, TaskScheduler.Default);
|
||||
|
||||
}
|
||||
|
||||
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
||||
RateFetcher rateFetcher, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
||||
|
||||
var ratesTask = rateFetcher.FetchRates(
|
||||
groupingByCurrency
|
||||
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
||||
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||
var invoices = await GetInvoicesForApp(appData, _InvoiceRepository);
|
||||
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray(),
|
||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray(),
|
||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||
|
||||
|
||||
var active = (settings.StartDate == null || DateTime.Now >= settings.StartDate) &&
|
||||
(settings.EndDate == null || DateTime.Now <= settings.EndDate) &&
|
||||
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
|
||||
|
||||
|
||||
|
||||
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,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = invoices.Length,
|
||||
CurrentPendingAmount = currentPendingAmount,
|
||||
CurrentAmount = currentAmount,
|
||||
Active = active,
|
||||
DaysLeft = settings.EndDate.HasValue? (settings.EndDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
DaysLeftToStart = settings.StartDate.HasValue? (settings.StartDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
ShowProgress = settings.TargetAmount.HasValue,
|
||||
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
||||
LastUpdated = DateTime.Now
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = $"{CrowdfundInvoiceOrderIdPrefix}{appData.Id}",
|
||||
Status = new string[]{
|
||||
InvoiceState.ToString(InvoiceStatus.New),
|
||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||
InvoiceState.ToString(InvoiceStatus.Complete)}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
253
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
253
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
|
@ -0,0 +1,253 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class CrowdfundHubStreamer
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app:";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
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 Dictionary<string, CancellationTokenSource> _CacheTokens = new Dictionary<string, CancellationTokenSource>();
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
AppsHelper appsHelper,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_MemoryCache = memoryCache;
|
||||
_AppsHelper = appsHelper;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
{
|
||||
var key = GetCacheKey(appId);
|
||||
return _MemoryCache.GetOrCreateAsync(key, async entry =>
|
||||
{
|
||||
if (_CacheTokens.ContainsKey(key))
|
||||
{
|
||||
_CacheTokens.Remove(key);
|
||||
}
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
var result = await GetInfo(app);
|
||||
entry.SetValue(result);
|
||||
|
||||
var token = new CancellationTokenSource();
|
||||
_CacheTokens.Add(key, token);
|
||||
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
|
||||
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()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
{
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
{
|
||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||
}
|
||||
|
||||
private void Subscription(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
if (!invoiceEvent.Invoice.OrderId.StartsWith(CrowdfundInvoiceOrderIdPrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var appId = invoiceEvent.Invoice.OrderId.Replace(CrowdfundInvoiceOrderIdPrefix, "", StringComparison.InvariantCultureIgnoreCase);
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
case InvoiceEvent.ReceivedPayment:
|
||||
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||
{
|
||||
invoiceEvent.Payment.GetCryptoPaymentData().GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
} );
|
||||
|
||||
InvalidateCacheForApp(appId);
|
||||
break;
|
||||
case InvoiceEvent.Completed:
|
||||
InvalidateCacheForApp(appId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCacheForApp(string appId)
|
||||
{
|
||||
if (_CacheTokens.ContainsKey(appId))
|
||||
{
|
||||
_CacheTokens[appId].Cancel();
|
||||
}
|
||||
|
||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
||||
{
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
||||
}, TaskScheduler.Default);
|
||||
|
||||
}
|
||||
|
||||
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
||||
RateFetcher rateFetcher, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
||||
|
||||
var ratesTask = rateFetcher.FetchRates(
|
||||
groupingByCurrency
|
||||
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
||||
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices)
|
||||
{
|
||||
var payments = invoices.SelectMany(entity => entity.GetPayments());
|
||||
|
||||
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
|
||||
|
||||
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
|
||||
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
|
||||
}
|
||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||
var invoices = await GetInvoicesForApp(appData, _InvoiceRepository);
|
||||
|
||||
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 currentAmount = await GetCurrentContributionAmount(
|
||||
completeInvoices,
|
||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
pendingInvoices,
|
||||
settings.TargetCurrency, _RateFetcher, rateRules);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices);
|
||||
|
||||
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,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = $"{CrowdfundInvoiceOrderIdPrefix}{appData.Id}",
|
||||
Status = new string[]{
|
||||
InvoiceState.ToString(InvoiceStatus.New),
|
||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||
InvoiceState.ToString(InvoiceStatus.Complete)}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
|
@ -32,21 +34,17 @@ namespace BTCPayServer.Models.AppViewModels
|
|||
public class CrowdfundInfo
|
||||
{
|
||||
public int TotalContributors { get; set; }
|
||||
public decimal CurrentAmount { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public bool ShowProgress { get; set; }
|
||||
public decimal? ProgressPercentage { get; set; }
|
||||
public int? DaysLeft{ get; set; }
|
||||
public int? DaysLeftToStart{ get; set; }
|
||||
public decimal CurrentPendingAmount { get; set; }
|
||||
public decimal CurrentAmount { get; set; }
|
||||
public decimal? ProgressPercentage { get; set; }
|
||||
public decimal? PendingProgressPercentage { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public Dictionary<string, decimal> PaymentStats { get; set; }
|
||||
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
{
|
||||
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
|
||||
|
|
|
@ -1,101 +1,101 @@
|
|||
@using BTCPayServer.Models.AppViewModels
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
<div class="container p-0" >
|
||||
|
||||
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<partial name="_StatusMessage" for="@Model.StatusMessage"/>
|
||||
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
|
||||
{
|
||||
<img class="card-img-top" src="@Model.MainImageUrl" alt="Card image cap">
|
||||
}
|
||||
@if (Model.Info.ShowProgress)
|
||||
{
|
||||
<div class="progress rounded-0 striped" style="min-height: 30px">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="@Model.Info.ProgressPercentage" aria-valuemin="0" aria-valuemax="100">
|
||||
@if (Model.Info.ProgressPercentage.Value > 0)
|
||||
{
|
||||
@(Model.Info.ProgressPercentage + "%")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="card-body">
|
||||
<div class="card-title row">
|
||||
<div class="col-md-9 col-sm-12">
|
||||
|
||||
<h1 >
|
||||
@Model.Title
|
||||
@if (!string.IsNullOrEmpty(Model.Tagline))
|
||||
{
|
||||
<h2 class="text-muted">@Model.Tagline</h2>
|
||||
}
|
||||
|
||||
@if (Model.Info.DaysLeftToStart.HasValue && Model.Info.DaysLeftToStart > 0)
|
||||
{
|
||||
<small>
|
||||
@($"{Model.Info.DaysLeftToStart} day{(Model.Info.DaysLeftToStart.Value > 1 ? "s" : "")} left to start")
|
||||
|
||||
</small>
|
||||
}
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
<ul class="list-group list-group-flush col-md-3 col-sm-12">
|
||||
<li class="list-group-item">@(Model.EndDate.HasValue? $"Ends {Model.EndDate.Value:dddd, dd MMMM yyyy HH:mm}" : "No specific end date")</li>
|
||||
<li class="list-group-item">@(Model.TargetAmount.HasValue? $"{Model.TargetAmount:G29} {Model.TargetCurrency.ToUpperInvariant()} Goal" :
|
||||
"No specific target goal")</li>
|
||||
<li class="list-group-item">@(Model.EnforceTargetAmount? $"Hardcap Goal" : "Softcap Goal")</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@if (Model.Info.Active)
|
||||
{
|
||||
<div class="card-deck mb-4 ">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">@Model.Info.TotalContributors</h5>
|
||||
<h6 class="card-text text-center"> Contributors</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">@Model.Info.CurrentAmount @Model.TargetCurrency.ToUpperInvariant()</h5>
|
||||
<h6 class="card-text text-center"> Raised</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Info.DaysLeft.HasValue && Model.Info.DaysLeft > 0)
|
||||
{
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">@Model.Info.DaysLeft</h5>
|
||||
<h6 class="card-text text-center">Day@(Model.Info.DaysLeft.Value > 1 ? "s" : "") left</h6>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<div class="card-text"> @Html.Raw(Model.Description)</div>
|
||||
@if (Model.Info.Active)
|
||||
{
|
||||
<hr/>
|
||||
<h3>Contribute</h3>
|
||||
<partial name="Crowdfund/ContributeForm" model="@(new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = true,
|
||||
ViewCrowdfundViewModel = Model
|
||||
})"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@* @using BTCPayServer.Models.AppViewModels *@
|
||||
@* @model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel *@
|
||||
@* <div class="container p-0" > *@
|
||||
@* *@
|
||||
@* *@
|
||||
@* <div class="row h-100 w-100 py-sm-0 py-md-4 mx-0"> *@
|
||||
@* *@
|
||||
@* <div class="card w-100 p-0 mx-0"> *@
|
||||
@* <partial name="_StatusMessage" for="@Model.StatusMessage"/> *@
|
||||
@* @if (!string.IsNullOrEmpty(Model.MainImageUrl)) *@
|
||||
@* { *@
|
||||
@* <img class="card-img-top" src="@Model.MainImageUrl" alt="Card image cap"> *@
|
||||
@* } *@
|
||||
@* @if (Model.Info.ShowProgress) *@
|
||||
@* { *@
|
||||
@* <div class="progress rounded-0 striped" style="min-height: 30px"> *@
|
||||
@* <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="@Model.Info.ProgressPercentage" aria-valuemin="0" aria-valuemax="100"> *@
|
||||
@* @if (Model.Info.ProgressPercentage.Value > 0) *@
|
||||
@* { *@
|
||||
@* @(Model.Info.ProgressPercentage + "%") *@
|
||||
@* } *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* } *@
|
||||
@* <div class="card-body"> *@
|
||||
@* <div class="card-title row"> *@
|
||||
@* <div class="col-md-9 col-sm-12"> *@
|
||||
@* *@
|
||||
@* <h1 > *@
|
||||
@* @Model.Title *@
|
||||
@* @if (!string.IsNullOrEmpty(Model.Tagline)) *@
|
||||
@* { *@
|
||||
@* <h2 class="text-muted">@Model.Tagline</h2> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* @if (Model.Info.DaysLeftToStart.HasValue && Model.Info.DaysLeftToStart > 0) *@
|
||||
@* { *@
|
||||
@* <small> *@
|
||||
@* @($"{Model.Info.DaysLeftToStart} day{(Model.Info.DaysLeftToStart.Value > 1 ? "s" : "")} left to start") *@
|
||||
@* *@
|
||||
@* </small> *@
|
||||
@* } *@
|
||||
@* </h1> *@
|
||||
@* *@
|
||||
@* </div> *@
|
||||
@* <ul class="list-group list-group-flush col-md-3 col-sm-12"> *@
|
||||
@* <li class="list-group-item">@(Model.EndDate.HasValue? $"Ends {Model.EndDate.Value:dddd, dd MMMM yyyy HH:mm}" : "No specific end date")</li> *@
|
||||
@* <li class="list-group-item">@(Model.TargetAmount.HasValue? $"{Model.TargetAmount:G29} {Model.TargetCurrency.ToUpperInvariant()} Goal" : *@
|
||||
@* "No specific target goal")</li> *@
|
||||
@* <li class="list-group-item">@(Model.EnforceTargetAmount? $"Hardcap Goal" : "Softcap Goal")</li> *@
|
||||
@* </ul> *@
|
||||
@* *@
|
||||
@* </div> *@
|
||||
@* @if (Model.Info.Active) *@
|
||||
@* { *@
|
||||
@* <div class="card-deck mb-4 "> *@
|
||||
@* <div class="card shadow"> *@
|
||||
@* <div class="card-body"> *@
|
||||
@* <h5 class="card-title text-center">@Model.Info.TotalContributors</h5> *@
|
||||
@* <h6 class="card-text text-center"> Contributors</h6> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* <div class="card shadow"> *@
|
||||
@* <div class="card-body"> *@
|
||||
@* <h5 class="card-title text-center">@Model.Info.CurrentAmount @Model.TargetCurrency.ToUpperInvariant()</h5> *@
|
||||
@* <h6 class="card-text text-center"> Raised</h6> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* @if (Model.Info.DaysLeft.HasValue && Model.Info.DaysLeft > 0) *@
|
||||
@* { *@
|
||||
@* <div class="card shadow"> *@
|
||||
@* <div class="card-body"> *@
|
||||
@* <h5 class="card-title text-center">@Model.Info.DaysLeft</h5> *@
|
||||
@* <h6 class="card-text text-center">Day@(Model.Info.DaysLeft.Value > 1 ? "s" : "") left</h6> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* *@
|
||||
@* </div> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* *@
|
||||
@* <div class="card-text"> @Html.Raw(Model.Description)</div> *@
|
||||
@* @if (Model.Info.Active) *@
|
||||
@* { *@
|
||||
@* <hr/> *@
|
||||
@* <h3>Contribute</h3> *@
|
||||
@* <partial name="Crowdfund/ContributeForm" model="@(new ContributeToCrowdfund() *@
|
||||
@* { *@
|
||||
@* RedirectToCheckout = true, *@
|
||||
@* ViewCrowdfundViewModel = Model *@
|
||||
@* })"/> *@
|
||||
@* } *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
|
||||
</div>
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.info.showProgress">
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
:aria-valuenow="srvModel.info.progressPercentage"
|
||||
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
||||
|
@ -45,8 +45,9 @@
|
|||
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<h5>{{srvModel.info.currentAmount + srvModel.info.currentPendingAmount }} {{targetCurrency}} </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
|
@ -72,13 +73,34 @@
|
|||
</h5>
|
||||
<h5 class="text-muted">Left to start</h5>
|
||||
</div>
|
||||
<div class="col-sm" v-if="ended">
|
||||
<div class="col-sm" v-if="ended" id="inactive-campaign">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
<b-tooltip target="inactive-campaign" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" style="list-style-type: none">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" style="list-style-type: none">
|
||||
{{ended? "Ended" : "End"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tooltip target="raised-amount" >
|
||||
<ul class="p-0">
|
||||
<li v-for="stat of paymentStats" style="list-style-type: none">
|
||||
{{stat.label}} {{stat.value}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
|
||||
<div class="row">
|
||||
|
@ -165,7 +187,7 @@
|
|||
|
||||
<script type="text/x-template" id="perks-template">
|
||||
<div >
|
||||
<perk :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active"
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund.min.js",
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle-1.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
|
@ -79,9 +79,8 @@
|
|||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.js",
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle-2.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/bundles/crowdfund.min.js",
|
||||
"wwwroot/vendor/moment/moment.js"
|
||||
],
|
||||
"minify": {
|
||||
|
|
|
@ -75,13 +75,50 @@ addLoadEvent(function (ev) {
|
|||
contributeModalOpen: false,
|
||||
endDiff: "",
|
||||
startDiff: "",
|
||||
active: true
|
||||
active: true,
|
||||
animation: true,
|
||||
sound: true
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
targetCurrency: function(){
|
||||
return this.srvModel.targetCurrency.toUpperCase();
|
||||
},
|
||||
paymentStats: function(){
|
||||
var result= [];
|
||||
|
||||
var combinedStats = {};
|
||||
|
||||
|
||||
var keys = Object.keys(this.srvModel.info.paymentStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if(combinedStats[keys[i]]){
|
||||
combinedStats[keys[i]] +=this.srvModel.info.paymentStats[keys[i]];
|
||||
}else{
|
||||
combinedStats[keys[i]] =this.srvModel.info.paymentStats[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
keys = Object.keys(this.srvModel.info.pendingPaymentStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if(combinedStats[keys[i]]){
|
||||
combinedStats[keys[i]] +=this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||
}else{
|
||||
combinedStats[keys[i]] =this.srvModel.info.pendingPaymentStats[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
keys = Object.keys(combinedStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")};
|
||||
result.push(newItem);
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
Loading…
Add table
Reference in a new issue