2023-03-20 10:39:26 +09:00
#nullable enable
2023-03-17 03:56:32 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading.Tasks ;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Abstractions.Contracts ;
using BTCPayServer.Abstractions.Models ;
using BTCPayServer.Abstractions.Services ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Configuration ;
using BTCPayServer.Data ;
using BTCPayServer.Plugins.Crowdfund.Controllers ;
using BTCPayServer.Plugins.Crowdfund.Models ;
2023-03-20 10:39:26 +09:00
using BTCPayServer.Plugins.PointOfSale ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Services ;
using BTCPayServer.Services.Apps ;
using BTCPayServer.Services.Invoices ;
using BTCPayServer.Services.Rates ;
using Ganss.XSS ;
using Microsoft.AspNetCore.Routing ;
2022-07-18 20:51:53 +02:00
using Microsoft.Extensions.DependencyInjection ;
2023-03-17 03:56:32 +01:00
using Microsoft.Extensions.Options ;
2022-07-18 20:51:53 +02:00
2023-03-17 03:56:32 +01:00
namespace BTCPayServer.Plugins.Crowdfund
2022-07-18 20:51:53 +02:00
{
public class CrowdfundPlugin : BaseBTCPayServerPlugin
{
public override string Identifier = > "BTCPayServer.Plugins.Crowdfund" ;
public override string Name = > "Crowdfund" ;
public override string Description = > "Create a self-hosted funding campaign, similar to Kickstarter or Indiegogo. Funds go directly to the creator’ s wallet without any fees." ;
public override void Execute ( IServiceCollection services )
{
2023-03-18 22:36:26 +01:00
services . AddSingleton < IUIExtension > ( new UIExtension ( "Crowdfund/NavExtension" , "header-nav" ) ) ;
2023-03-20 10:39:26 +09:00
services . AddSingleton < CrowdfundAppType > ( ) ;
services . AddSingleton < AppBaseType , CrowdfundAppType > ( ) ;
2023-04-10 11:07:03 +09:00
2022-07-18 20:51:53 +02:00
base . Execute ( services ) ;
}
}
2023-04-10 11:07:03 +09:00
public class CrowdfundAppType : AppBaseType , IHasSaleStatsAppType , IHasItemStatsAppType
2023-03-17 03:56:32 +01:00
{
private readonly LinkGenerator _linkGenerator ;
private readonly IOptions < BTCPayServerOptions > _options ;
private readonly DisplayFormatter _displayFormatter ;
private readonly CurrencyNameTable _currencyNameTable ;
private readonly HtmlSanitizer _htmlSanitizer ;
private readonly InvoiceRepository _invoiceRepository ;
public const string AppType = "Crowdfund" ;
2023-03-20 10:39:26 +09:00
public CrowdfundAppType (
2023-03-17 03:56:32 +01:00
LinkGenerator linkGenerator ,
IOptions < BTCPayServerOptions > options ,
InvoiceRepository invoiceRepository ,
DisplayFormatter displayFormatter ,
CurrencyNameTable currencyNameTable ,
HtmlSanitizer htmlSanitizer )
{
2023-03-20 10:39:26 +09:00
Description = Type = AppType ;
2023-03-17 03:56:32 +01:00
_linkGenerator = linkGenerator ;
_options = options ;
_displayFormatter = displayFormatter ;
_currencyNameTable = currencyNameTable ;
_htmlSanitizer = htmlSanitizer ;
_invoiceRepository = invoiceRepository ;
}
2023-03-20 10:39:26 +09:00
public override Task < string > ConfigureLink ( AppData app )
2023-03-17 03:56:32 +01:00
{
return Task . FromResult ( _linkGenerator . GetPathByAction ( nameof ( UICrowdfundController . UpdateCrowdfund ) ,
2023-03-20 10:39:26 +09:00
"UICrowdfund" , new { appId = app . Id } , _options . Value . RootPath ) ! ) ;
2023-03-17 03:56:32 +01:00
}
public Task < SalesStats > GetSalesStats ( AppData app , InvoiceEntity [ ] paidInvoices , int numberOfDays )
{
var cfS = app . GetSettings < CrowdfundSettings > ( ) ;
2023-05-23 02:18:57 +02:00
var items = AppService . Parse ( cfS . PerksTemplate ) ;
2023-03-17 03:56:32 +01:00
return AppService . GetSalesStatswithPOSItems ( items , paidInvoices , numberOfDays ) ;
}
public Task < IEnumerable < ItemStats > > GetItemStats ( AppData appData , InvoiceEntity [ ] paidInvoices )
{
var settings = appData . GetSettings < CrowdfundSettings > ( ) ;
2023-05-23 02:18:57 +02:00
var perks = AppService . Parse ( settings . PerksTemplate ) ;
2023-03-17 03:56:32 +01:00
var perkCount = paidInvoices
. Where ( entity = > entity . Currency . Equals ( settings . TargetCurrency , StringComparison . OrdinalIgnoreCase ) & &
// we need the item code to know which perk it is and group by that
! string . IsNullOrEmpty ( entity . Metadata . ItemCode ) )
. GroupBy ( entity = > entity . Metadata . ItemCode )
. Select ( entities = >
{
var total = entities
. Sum ( entity = > entity . GetPayments ( true )
. Sum ( pay = >
{
var paymentMethodId = pay . GetPaymentMethodId ( ) ;
var value = pay . GetCryptoPaymentData ( ) . GetValue ( ) - pay . NetworkFee ;
var rate = entity . GetPaymentMethod ( paymentMethodId ) . Rate ;
return rate * value ;
} ) ) ;
var itemCode = entities . Key ;
var perk = perks . FirstOrDefault ( p = > p . Id = = itemCode ) ;
return new ItemStats
{
ItemCode = itemCode ,
Title = perk ? . Title ? ? itemCode ,
SalesCount = entities . Count ( ) ,
Total = total ,
TotalFormatted = _displayFormatter . Currency ( total , settings . TargetCurrency )
} ;
} )
. OrderByDescending ( stats = > stats . SalesCount ) ;
return Task . FromResult < IEnumerable < ItemStats > > ( perkCount ) ;
}
2023-03-20 10:39:26 +09:00
public override async Task < object? > GetInfo ( AppData appData )
2023-03-17 03:56:32 +01:00
{
var settings = appData . GetSettings < CrowdfundSettings > ( ) ;
var resetEvery = settings . StartDate . HasValue ? settings . ResetEvery : CrowdfundResetEvery . Never ;
DateTime ? lastResetDate = null ;
DateTime ? nextResetDate = null ;
2023-03-20 10:39:26 +09:00
if ( resetEvery ! = CrowdfundResetEvery . Never & & settings . StartDate is not null )
2023-03-17 03:56:32 +01:00
{
lastResetDate = settings . StartDate . Value ;
nextResetDate = lastResetDate . Value ;
while ( DateTime . UtcNow > = 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 ;
}
}
}
2023-04-10 11:07:03 +09:00
var invoices = await AppService . GetInvoicesForApp ( _invoiceRepository , appData , lastResetDate ) ;
2023-03-17 03:56:32 +01:00
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 perkCount = paidInvoices
. Where ( entity = > ! string . IsNullOrEmpty ( entity . Metadata . ItemCode ) )
. GroupBy ( entity = > entity . Metadata . ItemCode )
. ToDictionary ( entities = > entities . Key , entities = > entities . Count ( ) ) ;
Dictionary < string , decimal > perkValue = new ( ) ;
if ( settings . DisplayPerksValue )
{
perkValue = paidInvoices
. Where ( entity = > entity . Currency . Equals ( settings . TargetCurrency , StringComparison . OrdinalIgnoreCase ) & &
! string . IsNullOrEmpty ( entity . Metadata . ItemCode ) )
. GroupBy ( entity = > entity . Metadata . ItemCode )
. ToDictionary ( entities = > entities . Key , entities = >
entities . Sum ( entity = > entity . GetPayments ( true ) . Sum ( pay = >
{
var paymentMethodId = pay . GetPaymentMethodId ( ) ;
var value = pay . GetCryptoPaymentData ( ) . GetValue ( ) - pay . NetworkFee ;
var rate = entity . GetPaymentMethod ( paymentMethodId ) . Rate ;
return rate * value ;
} ) ) ) ;
}
2023-05-23 02:18:57 +02:00
var perks = AppService . Parse ( settings . PerksTemplate , false ) ;
2023-03-17 03:56:32 +01:00
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 ) ;
2023-03-20 10:39:26 +09:00
perks = newPerksOrder . ToArray ( ) ! ;
2023-03-17 03:56:32 +01:00
}
var store = appData . StoreData ;
var storeBlob = store . GetStoreBlob ( ) ;
return new ViewCrowdfundViewModel
{
Title = settings . Title ,
Tagline = settings . Tagline ,
Description = settings . Description ,
CustomCSSLink = settings . CustomCSSLink ,
MainImageUrl = settings . MainImageUrl ,
EmbeddedCSS = settings . EmbeddedCSS ,
StoreName = store . StoreName ,
CssFileId = storeBlob . CssFileId ,
LogoFileId = storeBlob . LogoFileId ,
BrandColor = storeBlob . BrandColor ,
StoreId = appData . StoreDataId ,
AppId = appData . Id ,
StartDate = settings . StartDate ? . ToUniversalTime ( ) ,
EndDate = settings . EndDate ? . ToUniversalTime ( ) ,
TargetAmount = settings . TargetAmount ,
TargetCurrency = settings . TargetCurrency ,
EnforceTargetAmount = settings . EnforceTargetAmount ,
Perks = perks ,
Enabled = settings . Enabled ,
DisqusEnabled = settings . DisqusEnabled ,
SoundsEnabled = settings . SoundsEnabled ,
DisqusShortname = settings . DisqusShortname ,
AnimationsEnabled = settings . AnimationsEnabled ,
ResetEveryAmount = settings . ResetEveryAmount ,
ResetEvery = Enum . GetName ( typeof ( CrowdfundResetEvery ) , settings . ResetEvery ) ,
DisplayPerksRanking = settings . DisplayPerksRanking ,
PerkCount = perkCount ,
PerkValue = perkValue ,
NeverReset = settings . ResetEvery = = CrowdfundResetEvery . Never ,
Sounds = settings . Sounds ,
AnimationColors = settings . AnimationColors ,
CurrencyData = _currencyNameTable . GetCurrencyData ( settings . TargetCurrency , true ) ,
CurrencyDataPayments = currentPayments . Select ( pair = > pair . Key )
. Concat ( pendingPayments . Select ( pair = > pair . Key ) )
. Select ( id = > _currencyNameTable . GetCurrencyData ( id . CryptoCode , true ) ) . DistinctBy ( data = > data . Code )
. ToDictionary ( data = > data . Code , data = > data ) ,
Info = new ViewCrowdfundViewModel . CrowdfundInfo
{
TotalContributors = paidInvoices . Length ,
ProgressPercentage = ( currentPayments . TotalCurrency / settings . TargetAmount ) * 100 ,
PendingProgressPercentage = ( pendingPayments . TotalCurrency / settings . TargetAmount ) * 100 ,
LastUpdated = DateTime . UtcNow ,
PaymentStats = currentPayments . ToDictionary ( c = > c . Key . ToString ( ) , c = > c . Value . Value ) ,
PendingPaymentStats = pendingPayments . ToDictionary ( c = > c . Key . ToString ( ) , c = > c . Value . Value ) ,
LastResetDate = lastResetDate ,
NextResetDate = nextResetDate ,
CurrentPendingAmount = pendingPayments . TotalCurrency ,
CurrentAmount = currentPayments . TotalCurrency
}
} ;
}
2023-03-20 10:39:26 +09:00
public override Task SetDefaultSettings ( AppData appData , string defaultCurrency )
2023-03-17 03:56:32 +01:00
{
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency } ;
appData . SetSettings ( emptyCrowdfund ) ;
return Task . CompletedTask ;
}
2023-03-20 10:39:26 +09:00
public override Task < string > ViewLink ( AppData app )
2023-03-17 03:56:32 +01:00
{
return Task . FromResult ( _linkGenerator . GetPathByAction ( nameof ( UICrowdfundController . ViewCrowdfund ) ,
2023-04-10 11:07:03 +09:00
"UICrowdfund" , new { appId = app . Id } , _options . Value . RootPath ) ! ) ;
2023-03-17 03:56:32 +01:00
}
private static bool IsPaid ( InvoiceEntity entity )
{
return entity . Status = = InvoiceStatusLegacy . Complete | | entity . Status = = InvoiceStatusLegacy . Confirmed | | entity . Status = = InvoiceStatusLegacy . Paid ;
}
private static bool IsPending ( InvoiceEntity entity )
{
return ! ( entity . Status = = InvoiceStatusLegacy . Complete | | entity . Status = = InvoiceStatusLegacy . Confirmed ) ;
}
private static bool IsComplete ( InvoiceEntity entity )
{
return entity . Status = = InvoiceStatusLegacy . Complete | | entity . Status = = InvoiceStatusLegacy . Confirmed ;
}
}
2022-07-18 20:51:53 +02:00
}