2020-06-29 04:44:35 +02:00
using System ;
2020-06-24 03:34:09 +02:00
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
using System.Threading.Channels ;
using System.Threading.Tasks ;
2022-06-28 16:02:17 +02:00
using BTCPayServer.Abstractions.Extensions ;
2021-04-13 10:36:49 +02:00
using BTCPayServer.Client.Models ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Data ;
2024-09-02 11:37:39 +02:00
using BTCPayServer.Events ;
2024-10-19 14:33:34 +02:00
using BTCPayServer.Lightning ;
2021-11-22 09:16:08 +01:00
using BTCPayServer.Logging ;
2022-06-28 16:02:17 +02:00
using BTCPayServer.Models.WalletViewModels ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Payments ;
2024-05-01 03:22:07 +02:00
using BTCPayServer.Payouts ;
2022-06-28 16:02:17 +02:00
using BTCPayServer.Rating ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Services ;
2024-04-04 09:31:04 +02:00
using BTCPayServer.Services.Invoices ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Services.Notifications ;
using BTCPayServer.Services.Notifications.Blobs ;
using BTCPayServer.Services.Rates ;
2024-10-19 14:33:34 +02:00
using Dapper ;
2020-06-24 03:34:09 +02:00
using Microsoft.EntityFrameworkCore ;
2021-07-16 09:57:37 +02:00
using Microsoft.Extensions.Logging ;
2020-06-24 03:34:09 +02:00
using NBitcoin ;
using NBitcoin.DataEncoders ;
2021-04-13 10:36:49 +02:00
using NBXplorer ;
2024-10-19 14:33:34 +02:00
using Newtonsoft.Json ;
2022-11-15 10:40:57 +01:00
using Newtonsoft.Json.Linq ;
2021-04-13 10:36:49 +02:00
using PayoutData = BTCPayServer . Data . PayoutData ;
2022-06-28 16:02:17 +02:00
using PullPaymentData = BTCPayServer . Data . PullPaymentData ;
2021-04-13 10:36:49 +02:00
2020-06-24 03:34:09 +02:00
namespace BTCPayServer.HostedServices
{
public class CreatePullPayment
{
public DateTimeOffset ? ExpiresAt { get ; set ; }
public DateTimeOffset ? StartsAt { get ; set ; }
public string StoreId { get ; set ; }
public string Name { get ; set ; }
2022-02-10 06:54:00 +01:00
public string Description { get ; set ; }
2020-06-24 03:34:09 +02:00
public decimal Amount { get ; set ; }
public string Currency { get ; set ; }
2024-09-26 04:25:45 +02:00
public PayoutMethodId [ ] PayoutMethods { get ; set ; }
2022-04-28 02:51:04 +02:00
public bool AutoApproveClaims { get ; set ; }
2022-01-24 12:17:09 +01:00
public TimeSpan ? BOLT11Expiration { get ; set ; }
2020-06-24 03:34:09 +02:00
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public class PullPaymentHostedService : BaseAsyncService
{
2023-06-16 03:56:17 +02:00
private readonly string [ ] _lnurlSupportedCurrencies = { "BTC" , "SATS" } ;
2024-05-17 07:46:17 +02:00
2020-06-24 03:34:09 +02:00
public class CancelRequest
{
public CancelRequest ( string pullPaymentId )
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( pullPaymentId ) ;
2020-06-24 03:34:09 +02:00
PullPaymentId = pullPaymentId ;
}
2022-04-24 05:19:34 +02:00
2022-11-15 10:40:57 +01:00
public CancelRequest ( string [ ] payoutIds , string [ ] storeIds )
2020-06-24 03:34:09 +02:00
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( payoutIds ) ;
2020-06-24 03:34:09 +02:00
PayoutIds = payoutIds ;
2022-11-15 10:40:57 +01:00
StoreIds = storeIds ;
2020-06-24 03:34:09 +02:00
}
2022-04-24 05:19:34 +02:00
2022-11-15 10:40:57 +01:00
public string [ ] StoreIds { get ; set ; }
2020-06-24 03:34:09 +02:00
public string PullPaymentId { get ; set ; }
public string [ ] PayoutIds { get ; set ; }
2022-11-15 10:40:57 +01:00
internal TaskCompletionSource < Dictionary < string , MarkPayoutRequest . PayoutPaidResult > > Completion { get ; set ; }
2020-06-24 03:34:09 +02:00
}
2022-04-24 05:19:34 +02:00
2020-06-24 06:44:26 +02:00
public class PayoutApproval
{
public enum Result
{
Ok ,
NotFound ,
InvalidState ,
TooLowAmount ,
OldRevision
}
2022-04-24 05:19:34 +02:00
2022-12-04 13:23:59 +01:00
public record ApprovalResult ( Result Result , decimal? CryptoAmount ) ;
2023-01-06 14:18:07 +01:00
2020-06-24 06:44:26 +02:00
public string PayoutId { get ; set ; }
public int Revision { get ; set ; }
public decimal Rate { get ; set ; }
2022-12-04 13:23:59 +01:00
internal TaskCompletionSource < ApprovalResult > Completion { get ; set ; }
2020-06-24 03:34:09 +02:00
2020-06-24 06:44:26 +02:00
public static string GetErrorMessage ( Result result )
{
switch ( result )
{
case PullPaymentHostedService . PayoutApproval . Result . Ok :
return "Ok" ;
case PullPaymentHostedService . PayoutApproval . Result . InvalidState :
return "The payout is not in a state that can be approved" ;
case PullPaymentHostedService . PayoutApproval . Result . TooLowAmount :
return "The crypto amount is too small." ;
case PullPaymentHostedService . PayoutApproval . Result . OldRevision :
return "The crypto amount is too small." ;
case PullPaymentHostedService . PayoutApproval . Result . NotFound :
return "The payout is not found" ;
default :
throw new NotSupportedException ( ) ;
}
}
}
2024-05-17 07:46:17 +02:00
public Task < string > CreatePullPayment ( string storeId , CreatePullPaymentRequest request )
{
return CreatePullPayment ( new CreatePullPayment ( )
{
StartsAt = request . StartsAt ,
ExpiresAt = request . ExpiresAt ,
BOLT11Expiration = request . BOLT11Expiration ,
Name = request . Name ,
Description = request . Description ,
Amount = request . Amount ,
Currency = request . Currency ,
StoreId = storeId ,
2024-09-26 04:25:45 +02:00
PayoutMethods = request . PayoutMethods . Select ( p = > PayoutMethodId . Parse ( p ) ) . ToArray ( ) ,
2024-05-17 07:46:17 +02:00
AutoApproveClaims = request . AutoApproveClaims
} ) ;
}
2020-06-24 03:34:09 +02:00
public async Task < string > CreatePullPayment ( CreatePullPayment create )
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( create ) ;
2020-06-24 03:34:09 +02:00
if ( create . Amount < = 0.0 m )
throw new ArgumentException ( "Amount out of bound" , nameof ( create ) ) ;
using var ctx = this . _dbContextFactory . CreateContext ( ) ;
var o = new Data . PullPaymentData ( ) ;
2022-04-24 05:19:34 +02:00
o . StartDate = create . StartsAt is DateTimeOffset date
? date
: DateTimeOffset . UtcNow - TimeSpan . FromSeconds ( 1.0 ) ;
2020-06-24 03:34:09 +02:00
o . EndDate = create . ExpiresAt is DateTimeOffset date2 ? new DateTimeOffset ? ( date2 ) : null ;
o . Id = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 20 ) ) ;
o . StoreId = create . StoreId ;
2024-08-28 11:52:08 +02:00
o . Currency = create . Currency ;
o . Limit = create . Amount ;
2023-04-10 04:07:03 +02:00
2020-06-24 03:34:09 +02:00
o . SetBlob ( new PullPaymentBlob ( )
{
Name = create . Name ? ? string . Empty ,
2022-02-10 06:54:00 +01:00
Description = create . Description ? ? string . Empty ,
2024-09-26 04:25:45 +02:00
SupportedPayoutMethods = create . PayoutMethods ,
2022-04-28 02:51:04 +02:00
AutoApproveClaims = create . AutoApproveClaims ,
2024-05-09 02:18:02 +02:00
View = new PullPaymentBlob . PullPaymentView
2020-06-24 03:34:09 +02:00
{
Title = create . Name ? ? string . Empty ,
2022-02-10 06:54:00 +01:00
Description = create . Description ? ? string . Empty ,
2024-05-09 02:18:02 +02:00
Email = null
2022-01-24 12:17:09 +01:00
} ,
BOLT11Expiration = create . BOLT11Expiration ? ? TimeSpan . FromDays ( 30.0 )
2020-06-24 03:34:09 +02:00
} ) ;
ctx . PullPayments . Add ( o ) ;
await ctx . SaveChangesAsync ( ) ;
return o . Id ;
}
2022-08-17 09:45:51 +02:00
public class PayoutQuery
{
public PayoutState [ ] States { get ; set ; }
public string [ ] PullPayments { get ; set ; }
public string [ ] PayoutIds { get ; set ; }
2024-05-01 03:22:07 +02:00
public string [ ] PayoutMethods { get ; set ; }
2022-08-17 09:45:51 +02:00
public string [ ] Stores { get ; set ; }
2023-02-07 08:51:20 +01:00
public bool IncludeArchived { get ; set ; }
public bool IncludeStoreData { get ; set ; }
public bool IncludePullPaymentData { get ; set ; }
2023-09-19 02:55:15 +02:00
public DateTimeOffset ? From { get ; set ; }
public DateTimeOffset ? To { get ; set ; }
2024-10-19 17:08:28 +02:00
/// <summary>
/// All payouts are elligible for every processors with matching payout method.
/// However, some processor may be disabled for some payouts.
/// Setting this field will filter out payouts that have the processor disabled.
/// </summary>
public string Processor { get ; set ; }
2022-08-17 09:45:51 +02:00
}
public async Task < List < PayoutData > > GetPayouts ( PayoutQuery payoutQuery )
{
await using var ctx = _dbContextFactory . CreateContext ( ) ;
return await GetPayouts ( payoutQuery , ctx ) ;
}
2023-02-07 08:51:20 +01:00
public static async Task < List < PayoutData > > GetPayouts ( PayoutQuery payoutQuery , ApplicationDbContext ctx ,
CancellationToken cancellationToken = default )
2022-08-17 09:45:51 +02:00
{
var query = ctx . Payouts . AsQueryable ( ) ;
if ( payoutQuery . States is not null )
{
2024-02-08 08:44:03 +01:00
if ( payoutQuery . States . Length = = 1 )
{
var state = payoutQuery . States [ 0 ] ;
query = query . Where ( data = > data . State = = state ) ;
}
else
{
query = query . Where ( data = > payoutQuery . States . Contains ( data . State ) ) ;
}
2022-08-17 09:45:51 +02:00
}
if ( payoutQuery . PullPayments is not null )
{
query = query . Where ( data = > payoutQuery . PullPayments . Contains ( data . PullPaymentDataId ) ) ;
}
if ( payoutQuery . PayoutIds is not null )
{
2023-02-07 08:53:44 +01:00
if ( payoutQuery . PayoutIds . Length = = 1 )
{
var payoutId = payoutQuery . PayoutIds [ 0 ] ;
query = query . Where ( data = > data . Id = = payoutId ) ;
}
else
{
query = query . Where ( data = > payoutQuery . PayoutIds . Contains ( data . Id ) ) ;
}
2022-08-17 09:45:51 +02:00
}
2024-05-01 03:22:07 +02:00
if ( payoutQuery . PayoutMethods is not null )
2022-08-17 09:45:51 +02:00
{
2024-05-01 03:22:07 +02:00
if ( payoutQuery . PayoutMethods . Length = = 1 )
2024-02-08 08:44:03 +01:00
{
2024-05-01 03:22:07 +02:00
var pm = payoutQuery . PayoutMethods [ 0 ] ;
2024-06-28 13:07:53 +02:00
query = query . Where ( data = > pm = = data . PayoutMethodId ) ;
2024-02-08 08:44:03 +01:00
}
else
{
2024-06-28 13:07:53 +02:00
query = query . Where ( data = > payoutQuery . PayoutMethods . Contains ( data . PayoutMethodId ) ) ;
2024-02-08 08:44:03 +01:00
}
2022-08-17 09:45:51 +02:00
}
if ( payoutQuery . Stores is not null )
{
2024-02-08 08:44:03 +01:00
if ( payoutQuery . Stores . Length = = 1 )
{
var store = payoutQuery . Stores [ 0 ] ;
query = query . Where ( data = > store = = data . StoreDataId ) ;
}
else
{
query = query . Where ( data = > payoutQuery . Stores . Contains ( data . StoreDataId ) ) ;
}
2022-08-17 09:45:51 +02:00
}
2023-02-07 08:51:20 +01:00
if ( payoutQuery . IncludeStoreData )
{
query = query . Include ( data = > data . StoreData ) ;
}
2023-04-10 04:07:03 +02:00
2023-02-07 08:51:20 +01:00
if ( payoutQuery . IncludePullPaymentData | | ! payoutQuery . IncludeArchived )
{
query = query . Include ( data = > data . PullPaymentData ) ;
}
if ( ! payoutQuery . IncludeArchived )
{
query = query . Where ( data = >
data . PullPaymentData = = null | | ! data . PullPaymentData . Archived ) ;
}
2022-08-17 09:45:51 +02:00
2023-09-19 02:55:15 +02:00
if ( payoutQuery . From is not null )
{
query = query . Where ( data = > data . Date > = payoutQuery . From ) ;
}
if ( payoutQuery . To is not null )
{
query = query . Where ( data = > data . Date < = payoutQuery . To ) ;
}
2024-10-19 17:08:28 +02:00
if ( payoutQuery . Processor is not null )
{
var q = new JObject ( )
{
["DisabledProcessors"] = new JArray ( payoutQuery . Processor )
} . ToString ( ) ;
query = query . Where ( data = > ! EF . Functions . JsonContains ( data . Blob , q ) ) ;
}
2023-02-07 08:51:20 +01:00
return await query . ToListAsync ( cancellationToken ) ;
2022-08-17 09:45:51 +02:00
}
2021-06-10 11:54:27 +02:00
public async Task < Data . PullPaymentData > GetPullPayment ( string pullPaymentId , bool includePayouts )
2020-06-24 03:34:09 +02:00
{
2021-06-10 11:43:45 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2021-06-10 11:54:27 +02:00
IQueryable < Data . PullPaymentData > query = ctx . PullPayments ;
if ( includePayouts )
query = query . Include ( data = > data . Payouts ) ;
return await query . FirstOrDefaultAsync ( data = > data . Id = = pullPaymentId ) ;
2020-06-24 03:34:09 +02:00
}
2024-09-02 11:37:39 +02:00
record TopUpRequest ( string PullPaymentId , InvoiceEntity InvoiceEntity ) ;
2020-06-24 03:34:09 +02:00
class PayoutRequest
{
2022-04-24 05:19:34 +02:00
public PayoutRequest ( TaskCompletionSource < ClaimRequest . ClaimResponse > completionSource ,
ClaimRequest request )
2020-06-24 03:34:09 +02:00
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( request ) ;
ArgumentNullException . ThrowIfNull ( completionSource ) ;
2020-06-24 03:34:09 +02:00
Completion = completionSource ;
ClaimRequest = request ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public TaskCompletionSource < ClaimRequest . ClaimResponse > Completion { get ; set ; }
public ClaimRequest ClaimRequest { get ; }
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public PullPaymentHostedService ( ApplicationDbContextFactory dbContextFactory ,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings ,
EventAggregator eventAggregator ,
BTCPayNetworkProvider networkProvider ,
2024-05-01 03:22:07 +02:00
PayoutMethodHandlerDictionary handlers ,
2024-05-13 15:29:42 +02:00
DefaultRulesCollection defaultRules ,
2020-06-24 06:44:26 +02:00
NotificationSender notificationSender ,
2021-04-13 10:36:49 +02:00
RateFetcher rateFetcher ,
2021-11-22 09:16:08 +01:00
ILogger < PullPaymentHostedService > logger ,
2022-06-28 16:02:17 +02:00
Logs logs ,
2023-03-13 02:12:58 +01:00
DisplayFormatter displayFormatter ,
2022-06-28 16:02:17 +02:00
CurrencyNameTable currencyNameTable ) : base ( logs )
2020-06-24 03:34:09 +02:00
{
_dbContextFactory = dbContextFactory ;
_jsonSerializerSettings = jsonSerializerSettings ;
_eventAggregator = eventAggregator ;
_networkProvider = networkProvider ;
2024-04-04 09:31:04 +02:00
_handlers = handlers ;
2024-05-13 15:29:42 +02:00
_defaultRules = defaultRules ;
2020-06-24 03:34:09 +02:00
_notificationSender = notificationSender ;
2020-06-24 06:44:26 +02:00
_rateFetcher = rateFetcher ;
2021-07-16 09:57:37 +02:00
_logger = logger ;
2022-06-28 16:02:17 +02:00
_currencyNameTable = currencyNameTable ;
2023-03-13 02:12:58 +01:00
_displayFormatter = displayFormatter ;
2020-06-24 03:34:09 +02:00
}
Channel < object > _Channel ;
private readonly ApplicationDbContextFactory _dbContextFactory ;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings ;
private readonly EventAggregator _eventAggregator ;
private readonly BTCPayNetworkProvider _networkProvider ;
2024-05-01 03:22:07 +02:00
private readonly PayoutMethodHandlerDictionary _handlers ;
2024-05-13 15:29:42 +02:00
private readonly DefaultRulesCollection _defaultRules ;
2020-06-24 03:34:09 +02:00
private readonly NotificationSender _notificationSender ;
2020-06-24 06:44:26 +02:00
private readonly RateFetcher _rateFetcher ;
2021-07-16 09:57:37 +02:00
private readonly ILogger < PullPaymentHostedService > _logger ;
2022-06-28 16:02:17 +02:00
private readonly CurrencyNameTable _currencyNameTable ;
2023-03-13 02:12:58 +01:00
private readonly DisplayFormatter _displayFormatter ;
2021-04-13 10:36:49 +02:00
private readonly CompositeDisposable _subscriptions = new CompositeDisposable ( ) ;
2020-06-24 03:34:09 +02:00
internal override Task [ ] InitializeTasks ( )
{
_Channel = Channel . CreateUnbounded < object > ( ) ;
2024-05-01 03:22:07 +02:00
foreach ( IPayoutHandler payoutHandler in _handlers )
2021-04-13 10:36:49 +02:00
{
payoutHandler . StartBackgroundCheck ( Subscribe ) ;
}
2024-09-02 11:37:39 +02:00
_eventAggregator . Subscribe < Events . InvoiceEvent > ( TopUpInvoiceCore ) ;
2023-01-06 14:18:07 +01:00
return new [ ] { Loop ( ) } ;
2020-06-24 03:34:09 +02:00
}
2024-09-02 11:37:39 +02:00
private void TopUpInvoiceCore ( InvoiceEvent evt )
{
if ( evt . EventCode = = InvoiceEventCode . Completed | | evt . EventCode = = InvoiceEventCode . MarkedCompleted )
{
foreach ( var pullPaymentId in evt . Invoice . GetInternalTags ( "PULLPAY#" ) )
{
_Channel . Writer . TryWrite ( new TopUpRequest ( pullPaymentId , evt . Invoice ) ) ;
}
}
}
2021-04-13 10:36:49 +02:00
private void Subscribe ( params Type [ ] events )
{
foreach ( Type @event in events )
{
_eventAggregator . Subscribe ( @event , ( subscription , o ) = > _Channel . Writer . TryWrite ( o ) ) ;
}
}
2020-06-24 03:34:09 +02:00
private async Task Loop ( )
{
await foreach ( var o in _Channel . Reader . ReadAllAsync ( ) )
{
2024-09-02 11:37:39 +02:00
if ( o is TopUpRequest topUp )
{
await HandleTopUp ( topUp ) ;
}
2020-06-24 03:34:09 +02:00
if ( o is PayoutRequest req )
{
await HandleCreatePayout ( req ) ;
}
2020-06-24 06:44:26 +02:00
if ( o is PayoutApproval approv )
{
await HandleApproval ( approv ) ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
if ( o is CancelRequest cancel )
{
await HandleCancel ( cancel ) ;
2021-06-10 11:54:27 +02:00
}
2022-04-24 05:19:34 +02:00
2021-06-10 11:43:45 +02:00
if ( o is InternalPayoutPaidRequest paid )
{
await HandleMarkPaid ( paid ) ;
2021-06-10 11:54:27 +02:00
}
2022-04-24 05:19:34 +02:00
2024-05-01 03:22:07 +02:00
foreach ( IPayoutHandler payoutHandler in _handlers )
2020-06-24 03:34:09 +02:00
{
2021-07-16 09:57:37 +02:00
try
{
await payoutHandler . BackgroundCheck ( o ) ;
}
catch ( Exception e )
{
_logger . LogError ( e , "PayoutHandler failed during BackgroundCheck" ) ;
}
2020-06-24 03:34:09 +02:00
}
}
}
2024-09-02 11:37:39 +02:00
private async Task HandleTopUp ( TopUpRequest topUp )
{
var pp = await this . GetPullPayment ( topUp . PullPaymentId , false ) ;
using var ctx = _dbContextFactory . CreateContext ( ) ;
var payout = new Data . PayoutData ( )
{
Id = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 20 ) ) ,
PayoutMethodId = PayoutMethodIds . TopUp . ToString ( ) ,
Date = DateTimeOffset . UtcNow ,
State = PayoutState . Completed ,
PullPaymentDataId = pp . Id ,
StoreDataId = pp . StoreId
} ;
if ( topUp . InvoiceEntity . Currency ! = pp . Currency | |
pp . Currency is not ( "SATS" or "BTC" ) )
return ;
payout . Currency = pp . Currency ;
payout . Amount = - topUp . InvoiceEntity . Price ;
payout . OriginalCurrency = payout . Currency ;
payout . OriginalAmount = payout . Amount . Value ;
var payoutBlob = new PayoutBlob ( )
{
Destination = topUp . InvoiceEntity . Id ,
Metadata = new JObject ( )
} ;
payout . SetBlob ( payoutBlob , _jsonSerializerSettings ) ;
await ctx . Payouts . AddAsync ( payout ) ;
await ctx . SaveChangesAsync ( ) ;
}
2024-08-28 11:52:08 +02:00
public bool SupportsLNURL ( PullPaymentData pp , PullPaymentBlob blob = null )
2023-06-16 03:56:17 +02:00
{
2024-08-28 11:52:08 +02:00
blob ? ? = pp . GetBlob ( ) ;
var pms = blob . SupportedPayoutMethods . FirstOrDefault ( id = >
2024-05-01 03:22:07 +02:00
PayoutTypes . LN . GetPayoutMethodId ( _networkProvider . DefaultNetwork . CryptoCode )
2024-04-04 09:31:04 +02:00
= = id ) ;
2024-08-28 11:52:08 +02:00
return pms is not null & & _lnurlSupportedCurrencies . Contains ( pp . Currency ) ;
2023-06-16 03:56:17 +02:00
}
2020-06-24 06:44:26 +02:00
public Task < RateResult > GetRate ( PayoutData payout , string explicitRateRule , CancellationToken cancellationToken )
{
2024-05-01 03:22:07 +02:00
var payoutPaymentMethod = payout . GetPayoutMethodId ( ) ;
2024-04-04 09:31:04 +02:00
var cryptoCode = _handlers . TryGetNetwork ( payoutPaymentMethod ) ? . NBXplorerNetwork . CryptoCode ;
var currencyPair = new Rating . CurrencyPair ( cryptoCode ,
2024-08-28 11:52:08 +02:00
payout . PullPaymentData ? . Currency ? ? cryptoCode ) ;
2020-06-24 06:44:26 +02:00
Rating . RateRule rule = null ;
try
{
if ( explicitRateRule is null )
{
2022-04-24 05:19:34 +02:00
var storeBlob = payout . StoreData . GetStoreBlob ( ) ;
2024-05-13 15:29:42 +02:00
var rules = storeBlob . GetRateRules ( _defaultRules ) ;
2020-06-24 06:44:26 +02:00
rules . Spread = 0.0 m ;
rule = rules . GetRuleFor ( currencyPair ) ;
}
else
{
rule = Rating . RateRule . CreateFromExpression ( explicitRateRule , currencyPair ) ;
}
}
catch ( Exception )
{
throw new FormatException ( "Invalid RateRule" ) ;
}
2022-04-24 05:19:34 +02:00
2024-04-30 11:31:15 +02:00
return _rateFetcher . FetchRate ( rule , new StoreIdRateContext ( payout . StoreDataId ) , cancellationToken ) ;
2020-06-24 06:44:26 +02:00
}
2022-04-24 05:19:34 +02:00
2022-12-04 13:23:59 +01:00
public Task < PayoutApproval . ApprovalResult > Approve ( PayoutApproval approval )
2020-06-24 06:44:26 +02:00
{
2022-04-24 05:19:34 +02:00
approval . Completion =
2022-12-04 13:23:59 +01:00
new TaskCompletionSource < PayoutApproval . ApprovalResult > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
2020-06-24 06:44:26 +02:00
if ( ! _Channel . Writer . TryWrite ( approval ) )
throw new ObjectDisposedException ( nameof ( PullPaymentHostedService ) ) ;
return approval . Completion . Task ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 06:44:26 +02:00
private async Task HandleApproval ( PayoutApproval req )
{
try
{
2023-07-20 15:05:14 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2022-04-24 05:19:34 +02:00
var payout = await ctx . Payouts . Include ( p = > p . PullPaymentData ) . Where ( p = > p . Id = = req . PayoutId )
. FirstOrDefaultAsync ( ) ;
2020-06-24 06:44:26 +02:00
if ( payout is null )
{
2022-12-04 13:23:59 +01:00
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . NotFound , null ) ) ;
2020-06-24 06:44:26 +02:00
return ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 06:44:26 +02:00
if ( payout . State ! = PayoutState . AwaitingApproval )
{
2022-12-04 13:23:59 +01:00
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . InvalidState , null ) ) ;
2020-06-24 06:44:26 +02:00
return ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 06:44:26 +02:00
var payoutBlob = payout . GetBlob ( this . _jsonSerializerSettings ) ;
if ( payoutBlob . Revision ! = req . Revision )
{
2022-12-04 13:23:59 +01:00
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . OldRevision , null ) ) ;
2020-06-24 06:44:26 +02:00
return ;
}
2022-04-24 05:19:34 +02:00
2024-06-28 13:07:53 +02:00
if ( ! PayoutMethodId . TryParse ( payout . PayoutMethodId , out var paymentMethod ) )
2021-09-24 07:16:25 +02:00
{
2022-12-04 13:23:59 +01:00
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . NotFound , null ) ) ;
2021-09-24 07:16:25 +02:00
return ;
}
2024-04-04 09:31:04 +02:00
var network = _handlers . TryGetNetwork ( paymentMethod ) ;
if ( network is null )
{
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . InvalidState , null ) ) ;
return ;
}
var cryptoCode = network . NBXplorerNetwork . CryptoCode ;
2020-06-24 06:44:26 +02:00
payout . State = PayoutState . AwaitingPayment ;
2021-12-31 08:59:02 +01:00
2022-08-17 09:45:51 +02:00
if ( payout . PullPaymentData is null | |
2024-08-28 11:52:08 +02:00
cryptoCode = = payout . PullPaymentData . Currency )
2020-06-24 06:44:26 +02:00
req . Rate = 1.0 m ;
2024-08-28 11:52:08 +02:00
var cryptoAmount = payout . OriginalAmount / req . Rate ;
2024-05-01 03:22:07 +02:00
var payoutHandler = _handlers . TryGet ( paymentMethod ) ;
2021-10-18 08:00:38 +02:00
if ( payoutHandler is null )
throw new InvalidOperationException ( $"No payout handler for {paymentMethod}" ) ;
2024-05-01 03:22:07 +02:00
var dest = await payoutHandler . ParseClaimDestination ( payoutBlob . Destination , default ) ;
2022-04-24 05:19:34 +02:00
decimal minimumCryptoAmount =
2024-05-01 03:22:07 +02:00
await payoutHandler . GetMinimumPayoutAmount ( dest . destination ) ;
2021-04-13 10:36:49 +02:00
if ( cryptoAmount < minimumCryptoAmount )
2020-06-24 06:44:26 +02:00
{
2022-12-04 13:23:59 +01:00
req . Completion . TrySetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . TooLowAmount , null ) ) ;
2024-10-19 14:33:34 +02:00
payout . State = PayoutState . Cancelled ;
await ctx . SaveChangesAsync ( ) ;
2020-06-24 06:44:26 +02:00
return ;
}
2022-04-24 05:19:34 +02:00
2024-08-28 11:52:08 +02:00
payout . Amount = Extensions . RoundUp ( cryptoAmount ,
2024-04-04 09:31:04 +02:00
network . Divisibility ) ;
2020-06-24 06:44:26 +02:00
await ctx . SaveChangesAsync ( ) ;
2022-04-24 05:19:34 +02:00
2023-07-20 15:05:14 +02:00
_eventAggregator . Publish ( new PayoutEvent ( PayoutEvent . PayoutEventType . Approved , payout ) ) ;
2024-08-28 11:52:08 +02:00
req . Completion . SetResult ( new PayoutApproval . ApprovalResult ( PayoutApproval . Result . Ok , payout . Amount ) ) ;
2020-06-24 06:44:26 +02:00
}
2020-06-28 10:55:27 +02:00
catch ( Exception ex )
2020-06-24 06:44:26 +02:00
{
req . Completion . TrySetException ( ex ) ;
}
}
2022-04-24 05:19:34 +02:00
2021-06-10 11:43:45 +02:00
private async Task HandleMarkPaid ( InternalPayoutPaidRequest req )
{
try
{
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2022-04-24 05:19:34 +02:00
var payout = await ctx . Payouts . Include ( p = > p . PullPaymentData ) . Where ( p = > p . Id = = req . Request . PayoutId )
. FirstOrDefaultAsync ( ) ;
2021-06-10 11:43:45 +02:00
if ( payout is null )
{
2022-11-15 10:40:57 +01:00
req . Completion . SetResult ( MarkPayoutRequest . PayoutPaidResult . NotFound ) ;
2021-06-10 11:43:45 +02:00
return ;
}
2022-04-24 05:19:34 +02:00
2022-11-15 10:40:57 +01:00
if ( payout . State = = PayoutState . Completed )
2021-06-10 11:43:45 +02:00
{
2022-11-15 10:40:57 +01:00
req . Completion . SetResult ( MarkPayoutRequest . PayoutPaidResult . InvalidState ) ;
2021-06-10 11:43:45 +02:00
return ;
}
2022-11-15 10:40:57 +01:00
switch ( req . Request . State )
2021-06-10 11:43:45 +02:00
{
2022-11-15 10:40:57 +01:00
case PayoutState . Completed or PayoutState . InProgress
2023-01-06 14:18:07 +01:00
when payout . State is not PayoutState . AwaitingPayment and not PayoutState . Completed and not PayoutState . InProgress :
2022-11-15 10:40:57 +01:00
case PayoutState . AwaitingPayment when payout . State is not PayoutState . InProgress :
req . Completion . SetResult ( MarkPayoutRequest . PayoutPaidResult . InvalidState ) ;
return ;
case PayoutState . InProgress or PayoutState . Completed :
payout . SetProofBlob ( req . Request . Proof ) ;
break ;
default :
payout . SetProofBlob ( null ) ;
break ;
2021-06-10 11:43:45 +02:00
}
2022-11-15 10:40:57 +01:00
payout . State = req . Request . State ;
2024-10-19 15:07:20 +02:00
if ( req . Request . UpdateBlob is { } b )
2024-10-19 14:33:34 +02:00
payout . SetBlob ( b , _jsonSerializerSettings ) ;
2021-06-10 11:43:45 +02:00
await ctx . SaveChangesAsync ( ) ;
2024-02-23 09:44:42 +01:00
_eventAggregator . Publish ( new PayoutEvent ( PayoutEvent . PayoutEventType . Updated , payout ) ) ;
2022-11-15 10:40:57 +01:00
req . Completion . SetResult ( MarkPayoutRequest . PayoutPaidResult . Ok ) ;
2021-06-10 11:43:45 +02:00
}
catch ( Exception ex )
{
req . Completion . TrySetException ( ex ) ;
}
}
2020-06-24 06:44:26 +02:00
2020-06-24 03:34:09 +02:00
private async Task HandleCreatePayout ( PayoutRequest req )
{
try
{
DateTimeOffset now = DateTimeOffset . UtcNow ;
2021-04-13 10:36:49 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2022-04-24 05:19:34 +02:00
var withoutPullPayment = req . ClaimRequest . PullPaymentId is null ;
var pp = string . IsNullOrEmpty ( req . ClaimRequest . PullPaymentId )
? null
: await ctx . PullPayments . FindAsync ( req . ClaimRequest . PullPaymentId ) ;
2021-05-13 10:50:08 +02:00
2022-04-24 05:19:34 +02:00
if ( ! withoutPullPayment & & ( pp is null | | pp . Archived ) )
2020-06-24 03:34:09 +02:00
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Archived ) ) ;
return ;
}
2022-04-24 05:19:34 +02:00
PullPaymentBlob ppBlob = null ;
if ( ! withoutPullPayment )
2020-06-24 03:34:09 +02:00
{
2022-04-24 05:19:34 +02:00
if ( pp . IsExpired ( now ) )
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Expired ) ) ;
return ;
}
if ( ! pp . HasStarted ( now ) )
{
req . Completion . TrySetResult (
new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . NotStarted ) ) ;
return ;
}
ppBlob = pp . GetBlob ( ) ;
2024-08-28 11:52:08 +02:00
if ( ! ppBlob . SupportedPayoutMethods . Contains ( req . ClaimRequest . PayoutMethodId ) )
2022-04-24 05:19:34 +02:00
{
req . Completion . TrySetResult (
new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . PaymentMethodNotSupported ) ) ;
return ;
}
2020-06-24 03:34:09 +02:00
}
2022-04-24 05:19:34 +02:00
2021-04-13 10:36:49 +02:00
var payoutHandler =
2024-05-01 03:22:07 +02:00
_handlers . TryGet ( req . ClaimRequest . PayoutMethodId ) ;
2022-04-24 05:19:34 +02:00
if ( payoutHandler is null )
2020-06-24 03:34:09 +02:00
{
2022-04-24 05:19:34 +02:00
req . Completion . TrySetResult (
new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . PaymentMethodNotSupported ) ) ;
2020-06-24 03:34:09 +02:00
return ;
}
2021-10-21 17:43:02 +02:00
if ( req . ClaimRequest . Destination . Id ! = null )
{
if ( await ctx . Payouts . AnyAsync ( data = >
2024-09-06 03:34:10 +02:00
data . DedupId . Equals ( req . ClaimRequest . Destination . Id ) & &
2022-04-24 05:19:34 +02:00
data . State ! = PayoutState . Completed & & data . State ! = PayoutState . Cancelled
2021-10-21 17:43:02 +02:00
) )
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Duplicate ) ) ;
return ;
}
}
2022-04-24 05:19:34 +02:00
var payoutsRaw = withoutPullPayment
? null
2024-05-01 10:59:10 +02:00
: await ctx . Payouts . Where ( p = > p . PullPaymentDataId = = pp . Id )
2022-04-24 05:19:34 +02:00
. Where ( p = > p . State ! = PayoutState . Cancelled ) . ToListAsync ( ) ;
2023-01-06 14:18:07 +01:00
var payouts = payoutsRaw ? . Select ( o = > new { Entity = o , Blob = o . GetBlob ( _jsonSerializerSettings ) } ) ;
2024-08-28 11:52:08 +02:00
var limit = pp ? . Limit ? ? 0 ;
var totalPayout = payouts ? . Select ( p = > p . Entity . OriginalAmount ) ? . Sum ( ) ;
2024-10-19 14:33:34 +02:00
var claimed = req . ClaimRequest . ClaimedAmount is decimal v ? v : limit - ( totalPayout ? ? 0 ) ;
2022-04-24 05:19:34 +02:00
if ( totalPayout is not null & & totalPayout + claimed > limit )
2020-06-24 03:34:09 +02:00
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Overdraft ) ) ;
return ;
}
2022-04-24 05:19:34 +02:00
if ( ! withoutPullPayment & & ( claimed < ppBlob . MinimumClaim | | claimed = = 0.0 m ) )
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . AmountTooLow ) ) ;
return ;
}
2020-06-24 03:34:09 +02:00
var payout = new PayoutData ( )
{
Id = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 20 ) ) ,
Date = now ,
2022-04-28 02:51:04 +02:00
State = PayoutState . AwaitingApproval ,
2020-06-24 03:34:09 +02:00
PullPaymentDataId = req . ClaimRequest . PullPaymentId ,
2024-06-28 13:07:53 +02:00
PayoutMethodId = req . ClaimRequest . PayoutMethodId . ToString ( ) ,
2024-09-06 03:34:10 +02:00
DedupId = req . ClaimRequest . Destination . Id ,
2024-06-28 13:07:53 +02:00
StoreDataId = req . ClaimRequest . StoreId ? ? pp ? . StoreId ,
2024-08-28 11:52:08 +02:00
Currency = payoutHandler . Currency ,
OriginalCurrency = pp ? . Currency ? ? payoutHandler . Currency
2020-06-24 03:34:09 +02:00
} ;
var payoutBlob = new PayoutBlob ( )
{
2023-07-24 11:37:18 +02:00
Destination = req . ClaimRequest . Destination . ToString ( ) ,
2024-05-17 07:46:17 +02:00
Metadata = req . ClaimRequest . Metadata ? ? new JObject ( ) ,
2020-06-24 03:34:09 +02:00
} ;
2024-08-28 11:52:08 +02:00
payout . OriginalAmount = claimed ;
2020-06-24 03:34:09 +02:00
payout . SetBlob ( payoutBlob , _jsonSerializerSettings ) ;
2021-04-13 10:36:49 +02:00
await ctx . Payouts . AddAsync ( payout ) ;
2020-06-24 03:34:09 +02:00
try
{
2023-04-07 08:58:41 +02:00
await payoutHandler . TrackClaim ( req . ClaimRequest , payout ) ;
2020-06-24 03:34:09 +02:00
await ctx . SaveChangesAsync ( ) ;
2023-07-20 15:05:14 +02:00
var response = new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Ok , payout ) ;
_eventAggregator . Publish ( new PayoutEvent ( PayoutEvent . PayoutEventType . Created , payout ) ) ;
2022-08-17 09:45:51 +02:00
if ( req . ClaimRequest . PreApprove . GetValueOrDefault ( ppBlob ? . AutoApproveClaims is true ) )
2022-04-28 02:51:04 +02:00
{
payout . StoreData = await ctx . Stores . FindAsync ( payout . StoreDataId ) ;
var rateResult = await GetRate ( payout , null , CancellationToken . None ) ;
if ( rateResult . BidAsk ! = null )
{
2022-12-04 13:23:59 +01:00
var approveResultTask = new TaskCompletionSource < PayoutApproval . ApprovalResult > ( ) ;
2022-04-28 02:51:04 +02:00
await HandleApproval ( new PayoutApproval ( )
{
2022-08-17 09:45:51 +02:00
PayoutId = payout . Id ,
Revision = payoutBlob . Revision ,
Rate = rateResult . BidAsk . Ask ,
2022-12-04 13:23:59 +01:00
Completion = approveResultTask
2022-04-28 02:51:04 +02:00
} ) ;
2022-12-04 13:23:59 +01:00
var approveResult = await approveResultTask . Task ;
if ( approveResult . Result = = PayoutApproval . Result . Ok )
2022-04-28 02:51:04 +02:00
{
payout . State = PayoutState . AwaitingPayment ;
2024-08-28 11:52:08 +02:00
payout . Amount = approveResult . CryptoAmount ;
2022-04-28 02:51:04 +02:00
}
2024-10-19 14:33:34 +02:00
else if ( approveResult . Result = = PayoutApproval . Result . TooLowAmount )
{
payout . State = PayoutState . Cancelled ;
await ctx . SaveChangesAsync ( ) ;
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . AmountTooLow ) ) ;
return ;
}
else
{
payout . State = PayoutState . Cancelled ;
await ctx . SaveChangesAsync ( ) ;
// We returns Ok even if the approval failed. This is expected.
// Because the claim worked, what didn't is the approval
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Ok ) ) ;
return ;
}
2022-04-28 02:51:04 +02:00
}
}
2022-08-17 09:45:51 +02:00
2023-07-20 15:05:14 +02:00
req . Completion . TrySetResult ( response ) ;
2022-04-24 05:19:34 +02:00
await _notificationSender . SendNotification ( new StoreScope ( payout . StoreDataId ) ,
new PayoutNotification ( )
{
StoreId = payout . StoreDataId ,
2024-08-28 11:52:08 +02:00
Currency = pp ? . Currency ? ? _handlers . TryGetNetwork ( req . ClaimRequest . PayoutMethodId ) ? . NBXplorerNetwork . CryptoCode ,
2022-04-24 05:19:34 +02:00
Status = payout . State ,
2024-06-28 13:07:53 +02:00
PaymentMethod = payout . PayoutMethodId ,
2022-04-24 05:19:34 +02:00
PayoutId = payout . Id
} ) ;
2020-06-24 03:34:09 +02:00
}
catch ( DbUpdateException )
{
req . Completion . TrySetResult ( new ClaimRequest . ClaimResponse ( ClaimRequest . ClaimResult . Duplicate ) ) ;
}
}
catch ( Exception ex )
{
req . Completion . TrySetException ( ex ) ;
}
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
private async Task HandleCancel ( CancelRequest cancel )
{
try
{
using var ctx = this . _dbContextFactory . CreateContext ( ) ;
List < PayoutData > payouts = null ;
if ( cancel . PullPaymentId ! = null )
{
2023-01-06 14:18:07 +01:00
ctx . PullPayments . Attach ( new Data . PullPaymentData ( ) { Id = cancel . PullPaymentId , Archived = true } )
2020-06-24 03:34:09 +02:00
. Property ( o = > o . Archived ) . IsModified = true ;
payouts = await ctx . Payouts
2022-04-24 05:19:34 +02:00
. Where ( p = > p . PullPaymentDataId = = cancel . PullPaymentId )
2023-01-06 14:18:07 +01:00
. Where ( p = > cancel . StoreIds = = null | | cancel . StoreIds . Contains ( p . StoreDataId ) )
2022-04-24 05:19:34 +02:00
. ToListAsync ( ) ;
2022-11-15 10:40:57 +01:00
cancel . PayoutIds = payouts . Select ( data = > data . Id ) . ToArray ( ) ;
2020-06-24 03:34:09 +02:00
}
else
{
var payoutIds = cancel . PayoutIds . ToHashSet ( ) ;
payouts = await ctx . Payouts
2022-04-24 05:19:34 +02:00
. Where ( p = > payoutIds . Contains ( p . Id ) )
2023-01-06 14:18:07 +01:00
. Where ( p = > cancel . StoreIds = = null | | cancel . StoreIds . Contains ( p . StoreDataId ) )
2022-04-24 05:19:34 +02:00
. ToListAsync ( ) ;
2020-06-24 03:34:09 +02:00
}
2022-11-15 10:40:57 +01:00
Dictionary < string , MarkPayoutRequest . PayoutPaidResult > result = new ( ) ;
2023-01-06 14:18:07 +01:00
2020-06-24 03:34:09 +02:00
foreach ( var payout in payouts )
{
if ( payout . State ! = PayoutState . Completed & & payout . State ! = PayoutState . InProgress )
2022-11-15 10:40:57 +01:00
{
2020-06-24 03:34:09 +02:00
payout . State = PayoutState . Cancelled ;
2023-01-06 14:18:07 +01:00
result . Add ( payout . Id , MarkPayoutRequest . PayoutPaidResult . Ok ) ;
2022-11-15 10:40:57 +01:00
}
else
{
2023-01-06 14:18:07 +01:00
result . Add ( payout . Id , MarkPayoutRequest . PayoutPaidResult . InvalidState ) ;
2022-11-15 10:40:57 +01:00
}
}
foreach ( string s1 in cancel . PayoutIds . Where ( s = > ! result . ContainsKey ( s ) ) )
{
result . Add ( s1 , MarkPayoutRequest . PayoutPaidResult . NotFound ) ;
2020-06-24 03:34:09 +02:00
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
await ctx . SaveChangesAsync ( ) ;
2023-12-01 10:50:05 +01:00
foreach ( var keyValuePair in result . Where ( pair = > pair . Value = = MarkPayoutRequest . PayoutPaidResult . Ok ) )
{
var payout = payouts . First ( p = > p . Id = = keyValuePair . Key ) ;
2024-02-23 09:44:42 +01:00
_eventAggregator . Publish ( new PayoutEvent ( PayoutEvent . PayoutEventType . Updated , payout ) ) ;
2023-12-01 10:50:05 +01:00
}
2022-11-15 10:40:57 +01:00
cancel . Completion . TrySetResult ( result ) ;
2020-06-24 03:34:09 +02:00
}
catch ( Exception ex )
{
cancel . Completion . TrySetException ( ex ) ;
}
}
2022-04-24 05:19:34 +02:00
2022-11-15 10:40:57 +01:00
public Task < Dictionary < string , MarkPayoutRequest . PayoutPaidResult > > Cancel ( CancelRequest cancelRequest )
2020-06-24 03:34:09 +02:00
{
CancellationToken . ThrowIfCancellationRequested ( ) ;
2023-01-06 14:18:07 +01:00
cancelRequest . Completion = new TaskCompletionSource < Dictionary < string , MarkPayoutRequest . PayoutPaidResult > > ( ) ;
2020-06-28 10:55:27 +02:00
if ( ! _Channel . Writer . TryWrite ( cancelRequest ) )
2020-06-24 06:44:26 +02:00
throw new ObjectDisposedException ( nameof ( PullPaymentHostedService ) ) ;
2023-01-06 14:18:07 +01:00
return cancelRequest . Completion . Task ;
2020-06-24 03:34:09 +02:00
}
public Task < ClaimRequest . ClaimResponse > Claim ( ClaimRequest request )
{
CancellationToken . ThrowIfCancellationRequested ( ) ;
2022-04-24 05:19:34 +02:00
var cts = new TaskCompletionSource < ClaimRequest . ClaimResponse > ( TaskCreationOptions
. RunContinuationsAsynchronously ) ;
2020-06-28 10:55:27 +02:00
if ( ! _Channel . Writer . TryWrite ( new PayoutRequest ( cts , request ) ) )
2020-06-24 06:44:26 +02:00
throw new ObjectDisposedException ( nameof ( PullPaymentHostedService ) ) ;
2020-06-24 03:34:09 +02:00
return cts . Task ;
}
public override Task StopAsync ( CancellationToken cancellationToken )
{
_Channel ? . Writer . Complete ( ) ;
2021-04-13 10:36:49 +02:00
_subscriptions . Dispose ( ) ;
2020-06-24 03:34:09 +02:00
return base . StopAsync ( cancellationToken ) ;
}
2021-06-10 11:43:45 +02:00
2022-11-15 10:40:57 +01:00
public Task < MarkPayoutRequest . PayoutPaidResult > MarkPaid ( MarkPayoutRequest request )
2021-06-10 11:43:45 +02:00
{
CancellationToken . ThrowIfCancellationRequested ( ) ;
2022-11-15 10:40:57 +01:00
var cts = new TaskCompletionSource < MarkPayoutRequest . PayoutPaidResult > ( TaskCreationOptions
2022-04-24 05:19:34 +02:00
. RunContinuationsAsynchronously ) ;
2021-06-10 11:43:45 +02:00
if ( ! _Channel . Writer . TryWrite ( new InternalPayoutPaidRequest ( cts , request ) ) )
throw new ObjectDisposedException ( nameof ( PullPaymentHostedService ) ) ;
return cts . Task ;
}
2022-08-17 09:45:51 +02:00
public PullPaymentsModel . PullPaymentModel . ProgressModel CalculatePullPaymentProgress ( PullPaymentData pp ,
DateTimeOffset now )
2022-06-28 16:02:17 +02:00
{
2024-08-28 11:52:08 +02:00
var ni = _currencyNameTable . GetCurrencyData ( pp . Currency , true ) ;
var nfi = _currencyNameTable . GetNumberFormatInfo ( pp . Currency , true ) ;
2024-05-01 10:59:10 +02:00
var totalCompleted = pp . Payouts
. Where ( p = > ( p . State = = PayoutState . Completed | |
p . State = = PayoutState . InProgress ) )
2024-08-28 11:52:08 +02:00
. Select ( o = > o . OriginalAmount ) . Sum ( ) . RoundToSignificant ( ni . Divisibility ) ;
2024-05-01 10:59:10 +02:00
var totalAwaiting = pp . Payouts
. Where ( p = > ( p . State = = PayoutState . AwaitingPayment | |
p . State = = PayoutState . AwaitingApproval ) ) . Select ( o = >
2024-08-28 11:52:08 +02:00
o . OriginalAmount ) . Sum ( ) . RoundToSignificant ( ni . Divisibility ) ;
2024-05-01 10:59:10 +02:00
2024-08-28 11:52:08 +02:00
var currencyData = _currencyNameTable . GetCurrencyData ( pp . Currency , true ) ;
2022-06-28 16:02:17 +02:00
return new PullPaymentsModel . PullPaymentModel . ProgressModel ( )
{
2024-08-28 11:52:08 +02:00
CompletedPercent = ( int ) ( totalCompleted / pp . Limit * 100 m ) ,
AwaitingPercent = ( int ) ( totalAwaiting / pp . Limit * 100 m ) ,
2022-06-28 16:02:17 +02:00
AwaitingFormatted = totalAwaiting . ToString ( "C" , nfi ) ,
Awaiting = totalAwaiting ,
Completed = totalCompleted ,
CompletedFormatted = totalCompleted . ToString ( "C" , nfi ) ,
2024-08-28 11:52:08 +02:00
Limit = pp . Limit . RoundToSignificant ( currencyData . Divisibility ) ,
LimitFormatted = _displayFormatter . Currency ( pp . Limit , pp . Currency ) ,
2024-05-01 10:59:10 +02:00
EndIn = pp . EndsIn ( ) is { } end ? end . TimeString ( ) : null ,
2022-06-28 16:02:17 +02:00
} ;
}
2022-08-17 09:45:51 +02:00
2022-06-28 16:02:17 +02:00
public TimeSpan ZeroIfNegative ( TimeSpan time )
{
if ( time < TimeSpan . Zero )
time = TimeSpan . Zero ;
return time ;
}
2022-08-17 09:45:51 +02:00
2024-09-02 11:37:39 +02:00
public static string GetInternalTag ( string ppId )
{
return $"PULLPAY#{ppId}" ;
}
2022-06-28 16:02:17 +02:00
2021-06-10 11:43:45 +02:00
class InternalPayoutPaidRequest
{
2022-11-15 10:40:57 +01:00
public InternalPayoutPaidRequest ( TaskCompletionSource < MarkPayoutRequest . PayoutPaidResult > completionSource ,
MarkPayoutRequest request )
2021-06-10 11:43:45 +02:00
{
2021-12-28 09:39:54 +01:00
ArgumentNullException . ThrowIfNull ( request ) ;
ArgumentNullException . ThrowIfNull ( completionSource ) ;
2021-06-10 11:43:45 +02:00
Completion = completionSource ;
Request = request ;
}
2022-04-24 05:19:34 +02:00
2022-11-15 10:40:57 +01:00
public TaskCompletionSource < MarkPayoutRequest . PayoutPaidResult > Completion { get ; set ; }
public MarkPayoutRequest Request { get ; }
2021-06-10 11:43:45 +02:00
}
}
2022-11-15 10:40:57 +01:00
public class MarkPayoutRequest
2021-06-10 11:43:45 +02:00
{
public enum PayoutPaidResult
{
Ok ,
NotFound ,
InvalidState
}
2022-04-24 05:19:34 +02:00
2021-06-10 11:43:45 +02:00
public string PayoutId { get ; set ; }
2022-11-18 16:04:46 +01:00
public JObject Proof { get ; set ; }
2023-02-07 08:51:20 +01:00
public PayoutState State { get ; set ; } = PayoutState . Completed ;
2024-10-19 15:07:20 +02:00
public PayoutBlob UpdateBlob { get ; internal set ; }
2021-06-10 11:54:27 +02:00
2021-06-10 11:43:45 +02:00
public static string GetErrorMessage ( PayoutPaidResult result )
{
switch ( result )
{
case PayoutPaidResult . NotFound :
return "The payout is not found" ;
case PayoutPaidResult . Ok :
return "Ok" ;
case PayoutPaidResult . InvalidState :
2022-11-15 10:40:57 +01:00
return "The payout is not in a state that can be marked with the specified state" ;
2021-06-10 11:43:45 +02:00
default :
throw new NotSupportedException ( ) ;
}
}
2020-06-24 03:34:09 +02:00
}
public class ClaimRequest
{
2024-10-19 14:33:34 +02:00
public record ClaimedAmountResult
{
public record Error ( string Message ) : ClaimedAmountResult ;
public record Success ( decimal? Amount ) : ClaimedAmountResult ;
}
public static ClaimedAmountResult GetClaimedAmount ( IClaimDestination destination , decimal? amount , string payoutCurrency , string ppCurrency )
2023-07-24 13:40:26 +02:00
{
2024-10-19 14:33:34 +02:00
var amountsComparable = false ;
var destinationAmount = destination . Amount ;
if ( destinationAmount is not null & &
payoutCurrency = = "BTC" & &
ppCurrency = = "SATS" )
{
destinationAmount = new LightMoney ( destinationAmount . Value , LightMoneyUnit . BTC ) . ToUnit ( LightMoneyUnit . Satoshi ) ;
amountsComparable = true ;
}
if ( destinationAmount is not null & & payoutCurrency = = ppCurrency )
{
amountsComparable = true ;
}
return ( destinationAmount , amount ) switch
2023-07-24 13:40:26 +02:00
{
2024-10-19 14:33:34 +02:00
( null , null ) when ppCurrency is null = > new ClaimedAmountResult . Error ( "Amount is not specified in destination or payout request" ) ,
( { } a , null ) when ppCurrency is null = > new ClaimedAmountResult . Success ( a ) ,
( null , null ) = > new ClaimedAmountResult . Success ( null ) ,
( { } a , null ) when amountsComparable = > new ClaimedAmountResult . Success ( a ) ,
( null , { } b ) = > new ClaimedAmountResult . Success ( b ) ,
( { } a , { } b ) when amountsComparable & & a = = b = > new ClaimedAmountResult . Success ( a ) ,
( { } a , { } b ) when amountsComparable & & a > b = > new ClaimedAmountResult . Error ( $"The destination's amount ({a} {ppCurrency}) is more than the claimed amount ({b} {ppCurrency})." ) ,
( { } a , { } b ) when amountsComparable & & a < b = > new ClaimedAmountResult . Success ( a ) ,
( { } a , { } b ) when ! amountsComparable = > new ClaimedAmountResult . Success ( b ) ,
_ = > new ClaimedAmountResult . Success ( amount )
2023-07-24 13:40:26 +02:00
} ;
}
2024-05-17 07:46:17 +02:00
2020-06-24 03:34:09 +02:00
public static string GetErrorMessage ( ClaimResult result )
{
switch ( result )
{
case ClaimResult . Ok :
break ;
case ClaimResult . Duplicate :
return "This address is already used for another payout" ;
case ClaimResult . Expired :
return "This pull payment is expired" ;
case ClaimResult . NotStarted :
return "This pull payment has yet started" ;
case ClaimResult . Archived :
return "This pull payment has been archived" ;
case ClaimResult . Overdraft :
return "The payout amount overdraft the pull payment's limit" ;
case ClaimResult . AmountTooLow :
return "The requested payout amount is too low" ;
case ClaimResult . PaymentMethodNotSupported :
return "This payment method is not supported by the pull payment" ;
default :
throw new NotSupportedException ( "Unsupported ClaimResult" ) ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
return null ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public class ClaimResponse
{
public ClaimResponse ( ClaimResult result , PayoutData payoutData = null )
{
Result = result ;
PayoutData = payoutData ;
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public ClaimResult Result { get ; set ; }
public PayoutData PayoutData { get ; set ; }
}
2022-04-24 05:19:34 +02:00
2020-06-24 03:34:09 +02:00
public enum ClaimResult
{
Ok ,
Duplicate ,
Expired ,
Archived ,
NotStarted ,
Overdraft ,
AmountTooLow ,
PaymentMethodNotSupported ,
}
2024-05-01 03:22:07 +02:00
public PayoutMethodId PayoutMethodId { get ; set ; }
2020-06-24 03:34:09 +02:00
public string PullPaymentId { get ; set ; }
2024-10-19 14:33:34 +02:00
public decimal? ClaimedAmount { get ; set ; }
2020-06-24 03:34:09 +02:00
public IClaimDestination Destination { get ; set ; }
2022-04-24 05:19:34 +02:00
public string StoreId { get ; set ; }
2022-04-28 02:51:04 +02:00
public bool? PreApprove { get ; set ; }
2023-07-24 11:37:18 +02:00
public JObject Metadata { get ; set ; }
2020-06-24 03:34:09 +02:00
}
2023-07-20 15:05:14 +02:00
2024-02-23 09:44:42 +01:00
public record PayoutEvent ( PayoutEvent . PayoutEventType Type , PayoutData Payout )
2023-07-20 15:05:14 +02:00
{
public enum PayoutEventType
{
Created ,
2023-12-01 10:50:05 +01:00
Approved ,
Updated
2023-07-20 15:05:14 +02:00
}
}
2020-06-24 03:34:09 +02:00
}