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 ;
2024-05-15 07:49:53 +09:00
using BTCPayServer.Client.Models ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Configuration ;
using BTCPayServer.Data ;
2023-12-01 16:13:44 +01:00
using BTCPayServer.Models ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Payments ;
using BTCPayServer.Payments.Lightning ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Plugins.Crowdfund.Controllers ;
using BTCPayServer.Plugins.Crowdfund.Models ;
using BTCPayServer.Services ;
using BTCPayServer.Services.Apps ;
using BTCPayServer.Services.Invoices ;
using BTCPayServer.Services.Rates ;
2023-10-18 19:33:43 +09:00
using Ganss.Xss ;
2024-05-09 02:18:02 +02:00
using Microsoft.AspNetCore.Http ;
2023-03-17 03:56:32 +01:00
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 ;
2024-04-04 16:31:04 +09:00
using static BTCPayServer . Plugins . Crowdfund . Models . ViewCrowdfundViewModel . CrowdfundInfo ;
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 InvoiceRepository _invoiceRepository ;
2024-04-04 16:31:04 +09:00
private readonly PrettyNameProvider _prettyNameProvider ;
2023-03-17 03:56:32 +01:00
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 ,
2024-04-04 16:31:04 +09:00
PrettyNameProvider prettyNameProvider ,
2023-03-17 03:56:32 +01:00
DisplayFormatter displayFormatter ,
2024-02-21 20:53:24 +01:00
CurrencyNameTable currencyNameTable )
2023-03-17 03:56:32 +01:00
{
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 ;
_invoiceRepository = invoiceRepository ;
2024-04-04 16:31:04 +09:00
_prettyNameProvider = prettyNameProvider ;
2023-03-17 03:56:32 +01:00
}
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 > ( ) ;
2024-04-04 16:31:04 +09: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 > ( ) ;
2024-04-04 16:31:04 +09: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 = >
{
2023-07-19 18:47:32 +09:00
var total = entities . Sum ( entity = > entity . PaidAmount . Net ) ;
2023-03-17 03:56:32 +01:00
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 > ( ) ;
2024-05-15 07:49:53 +09:00
var resetEvery = settings . StartDate . HasValue ? settings . ResetEvery : Services . Apps . CrowdfundResetEvery . Never ;
2023-03-17 03:56:32 +01:00
DateTime ? lastResetDate = null ;
DateTime ? nextResetDate = null ;
2024-05-15 07:49:53 +09:00
if ( resetEvery ! = Services . Apps . 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 )
{
2024-05-15 07:49:53 +09:00
case Services . Apps . CrowdfundResetEvery . Hour :
2023-03-17 03:56:32 +01:00
nextResetDate = lastResetDate . Value . AddHours ( settings . ResetEveryAmount ) ;
break ;
2024-05-15 07:49:53 +09:00
case Services . Apps . CrowdfundResetEvery . Day :
2023-03-17 03:56:32 +01:00
nextResetDate = lastResetDate . Value . AddDays ( settings . ResetEveryAmount ) ;
break ;
2024-05-15 07:49:53 +09:00
case Services . Apps . CrowdfundResetEvery . Month :
2023-03-17 03:56:32 +01:00
nextResetDate = lastResetDate . Value . AddMonths ( settings . ResetEveryAmount ) ;
break ;
2024-05-15 07:49:53 +09:00
case Services . Apps . CrowdfundResetEvery . Year :
2023-03-17 03:56:32 +01:00
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 = >
2023-07-19 18:47:32 +09:00
entities . Sum ( entity = > entity . PaidAmount . Net ) ) ;
2023-03-17 03:56:32 +01:00
}
2024-04-04 16:31:04 +09: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 ( ) ;
2024-02-21 13:41:21 +00:00
var formUrl = settings . FormId ! = null
? _linkGenerator . GetPathByAction ( nameof ( UICrowdfundController . CrowdfundForm ) , "UICrowdfund" ,
new { appId = appData . Id } , _options . Value . RootPath )
: null ;
2023-03-17 03:56:32 +01:00
return new ViewCrowdfundViewModel
{
Title = settings . Title ,
Tagline = settings . Tagline ,
Description = settings . Description ,
MainImageUrl = settings . MainImageUrl ,
StoreName = store . StoreName ,
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 ,
2024-05-15 07:49:53 +09:00
ResetEvery = Enum . GetName ( typeof ( Services . Apps . CrowdfundResetEvery ) , settings . ResetEvery ) ,
2023-03-17 03:56:32 +01:00
DisplayPerksRanking = settings . DisplayPerksRanking ,
PerkCount = perkCount ,
PerkValue = perkValue ,
2024-05-15 07:49:53 +09:00
NeverReset = settings . ResetEvery = = Services . Apps . CrowdfundResetEvery . Never ,
2024-02-21 13:41:21 +00:00
FormUrl = formUrl ,
2023-03-17 03:56:32 +01:00
Sounds = settings . Sounds ,
AnimationColors = settings . AnimationColors ,
CurrencyData = _currencyNameTable . GetCurrencyData ( settings . TargetCurrency , true ) ,
Info = new ViewCrowdfundViewModel . CrowdfundInfo
{
TotalContributors = paidInvoices . Length ,
ProgressPercentage = ( currentPayments . TotalCurrency / settings . TargetAmount ) * 100 ,
PendingProgressPercentage = ( pendingPayments . TotalCurrency / settings . TargetAmount ) * 100 ,
LastUpdated = DateTime . UtcNow ,
2024-04-04 16:31:04 +09:00
PaymentStats = GetPaymentStats ( currentPayments ) ,
PendingPaymentStats = GetPaymentStats ( pendingPayments ) ,
2023-03-17 03:56:32 +01:00
LastResetDate = lastResetDate ,
NextResetDate = nextResetDate ,
CurrentPendingAmount = pendingPayments . TotalCurrency ,
CurrentAmount = currentPayments . TotalCurrency
}
} ;
}
2024-04-04 16:31:04 +09:00
private Dictionary < string , PaymentStat > GetPaymentStats ( InvoiceStatistics stats )
{
var r = new Dictionary < string , PaymentStat > ( ) ;
var total = stats . Select ( s = > s . Value . CurrencyValue ) . Sum ( ) ;
foreach ( var kv in stats )
{
var pmi = PaymentMethodId . Parse ( kv . Key ) ;
r . TryAdd ( kv . Key , new PaymentStat ( )
{
Label = _prettyNameProvider . PrettyName ( pmi ) ,
Percent = ( kv . Value . CurrencyValue / total ) * 100.0 m ,
// Note that the LNURL will have the same LN
IsLightning = pmi = = PaymentTypes . LN . GetPaymentMethodId ( kv . Key )
} ) ;
}
return r ;
}
2023-03-20 10:39:26 +09:00
public override Task SetDefaultSettings ( AppData appData , string defaultCurrency )
2023-03-17 03:56:32 +01:00
{
2024-03-11 11:04:41 +01:00
var emptyCrowdfund = new CrowdfundSettings { Title = appData . Name , TargetCurrency = defaultCurrency } ;
2023-03-17 03:56:32 +01:00
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 )
{
2024-05-15 07:49:53 +09:00
return entity . Status = = InvoiceStatus . Settled | | entity . Status = = InvoiceStatus . Processing ;
2023-03-17 03:56:32 +01:00
}
private static bool IsPending ( InvoiceEntity entity )
{
2024-05-15 07:49:53 +09:00
return entity . Status ! = InvoiceStatus . Settled ;
2023-03-17 03:56:32 +01:00
}
private static bool IsComplete ( InvoiceEntity entity )
{
2024-05-15 07:49:53 +09:00
return entity . Status = = InvoiceStatus . Settled ;
2023-03-17 03:56:32 +01:00
}
}
2022-07-18 20:51:53 +02:00
}