2021-11-14 21:25:59 -08:00
#nullable enable
2020-06-28 21:44:35 -05:00
using System ;
2021-04-13 10:36:49 +02:00
using System.Collections.Generic ;
2020-06-24 10:34:09 +09:00
using System.Linq ;
2020-06-24 13:44:26 +09:00
using System.Threading ;
2020-06-24 10:34:09 +09:00
using System.Threading.Tasks ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Constants ;
2022-02-24 09:00:44 +01:00
using BTCPayServer.Abstractions.Extensions ;
2020-06-24 10:34:09 +09:00
using BTCPayServer.Client ;
using BTCPayServer.Client.Models ;
using BTCPayServer.Data ;
using BTCPayServer.HostedServices ;
using BTCPayServer.Payments ;
2023-01-26 01:46:05 +01:00
using BTCPayServer.Security ;
2020-06-24 10:34:09 +09:00
using BTCPayServer.Services ;
using BTCPayServer.Services.Rates ;
using Microsoft.AspNetCore.Authorization ;
2020-06-30 08:26:19 +02:00
using Microsoft.AspNetCore.Cors ;
2020-06-24 10:34:09 +09:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Routing ;
using Microsoft.EntityFrameworkCore ;
2022-11-15 10:40:57 +01:00
using MarkPayoutRequest = BTCPayServer . HostedServices . MarkPayoutRequest ;
2020-06-24 10:34:09 +09:00
2022-01-14 13:05:23 +09:00
namespace BTCPayServer.Controllers.Greenfield
2020-06-24 10:34:09 +09:00
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
2020-06-30 08:26:19 +02:00
[EnableCors(CorsPolicies.All)]
2020-06-24 10:34:09 +09:00
public class GreenfieldPullPaymentController : ControllerBase
{
private readonly PullPaymentHostedService _pullPaymentService ;
private readonly LinkGenerator _linkGenerator ;
private readonly ApplicationDbContextFactory _dbContextFactory ;
private readonly CurrencyNameTable _currencyNameTable ;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings ;
2021-04-13 10:36:49 +02:00
private readonly IEnumerable < IPayoutHandler > _payoutHandlers ;
2023-01-25 21:43:07 -08:00
private readonly BTCPayNetworkProvider _networkProvider ;
2023-01-26 01:46:05 +01:00
private readonly IAuthorizationService _authorizationService ;
2020-06-24 10:34:09 +09:00
public GreenfieldPullPaymentController ( PullPaymentHostedService pullPaymentService ,
LinkGenerator linkGenerator ,
ApplicationDbContextFactory dbContextFactory ,
CurrencyNameTable currencyNameTable ,
Services . BTCPayNetworkJsonSerializerSettings serializerSettings ,
2023-01-26 01:46:05 +01:00
IEnumerable < IPayoutHandler > payoutHandlers ,
2023-01-25 21:43:07 -08:00
BTCPayNetworkProvider btcPayNetworkProvider ,
2023-01-26 01:46:05 +01:00
IAuthorizationService authorizationService )
2020-06-24 10:34:09 +09:00
{
_pullPaymentService = pullPaymentService ;
_linkGenerator = linkGenerator ;
_dbContextFactory = dbContextFactory ;
_currencyNameTable = currencyNameTable ;
_serializerSettings = serializerSettings ;
2021-04-13 10:36:49 +02:00
_payoutHandlers = payoutHandlers ;
2023-01-25 21:43:07 -08:00
_networkProvider = btcPayNetworkProvider ;
2023-01-26 01:46:05 +01:00
_authorizationService = authorizationService ;
2020-06-24 10:34:09 +09:00
}
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > GetPullPayments ( string storeId , bool includeArchived = false )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
var pps = await ctx . PullPayments
. Where ( p = > p . StoreId = = storeId & & ( includeArchived | | ! p . Archived ) )
. OrderByDescending ( p = > p . StartDate )
. ToListAsync ( ) ;
return Ok ( pps . Select ( CreatePullPaymentData ) . ToArray ( ) ) ;
}
[HttpPost("~/api/v1/stores/{storeId}/pull-payments")]
2023-01-26 01:46:05 +01:00
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
2020-06-24 10:34:09 +09:00
public async Task < IActionResult > CreatePullPayment ( string storeId , CreatePullPaymentRequest request )
{
if ( request is null )
{
ModelState . AddModelError ( string . Empty , "Missing body" ) ;
return this . CreateValidationError ( ModelState ) ;
}
2023-01-26 01:46:05 +01:00
if ( request . AutoApproveClaims )
{
if ( ! ( await _authorizationService . AuthorizeAsync ( User , null ,
new PolicyRequirement ( Policies . CanCreatePullPayments ) ) ) . Succeeded )
{
return this . CreateAPIPermissionError ( Policies . CanCreatePullPayments ) ;
}
}
2020-06-24 10:34:09 +09:00
if ( request . Amount < = 0.0 m )
{
ModelState . AddModelError ( nameof ( request . Amount ) , "The amount should more than 0." ) ;
}
if ( request . Name is String name & & name . Length > 50 )
{
ModelState . AddModelError ( nameof ( request . Name ) , "The name should be maximum 50 characters." ) ;
}
if ( request . Currency is String currency )
{
2020-06-24 13:44:26 +09:00
request . Currency = currency . ToUpperInvariant ( ) . Trim ( ) ;
if ( _currencyNameTable . GetCurrencyData ( request . Currency , false ) is null )
2020-06-24 10:34:09 +09:00
{
2020-06-24 13:44:26 +09:00
ModelState . AddModelError ( nameof ( request . Currency ) , "Invalid currency" ) ;
2020-06-24 10:34:09 +09:00
}
}
else
{
ModelState . AddModelError ( nameof ( request . Currency ) , "This field is required" ) ;
}
if ( request . ExpiresAt is DateTimeOffset expires & & request . StartsAt is DateTimeOffset start & & expires < start )
{
ModelState . AddModelError ( nameof ( request . ExpiresAt ) , $"expiresAt should be higher than startAt" ) ;
}
if ( request . Period < = TimeSpan . Zero )
{
ModelState . AddModelError ( nameof ( request . Period ) , $"The period should be positive" ) ;
}
2022-01-24 20:17:09 +09:00
if ( request . BOLT11Expiration < = TimeSpan . Zero )
{
ModelState . AddModelError ( nameof ( request . BOLT11Expiration ) , $"The BOLT11 expiration should be positive" ) ;
}
2021-11-14 21:25:59 -08:00
PaymentMethodId ? [ ] ? paymentMethods = null ;
2021-10-18 05:37:59 +02:00
if ( request . PaymentMethods is { } paymentMethodsStr )
2020-06-24 10:34:09 +09:00
{
2021-10-18 05:37:59 +02:00
paymentMethods = paymentMethodsStr . Select ( s = >
2020-06-24 17:51:00 +09:00
{
2021-10-18 05:37:59 +02:00
PaymentMethodId . TryParse ( s , out var pmi ) ;
return pmi ;
} ) . ToArray ( ) ;
2021-12-31 16:59:02 +09:00
var supported = ( await _payoutHandlers . GetSupportedPaymentMethods ( HttpContext . GetStoreData ( ) ) ) . ToArray ( ) ;
for ( int i = 0 ; i < paymentMethods . Length ; i + + )
{
if ( ! supported . Contains ( paymentMethods [ i ] ) )
{
request . AddModelError ( paymentRequest = > paymentRequest . PaymentMethods [ i ] , "Invalid or unsupported payment method" , this ) ;
}
}
2020-06-24 10:34:09 +09:00
}
else
{
ModelState . AddModelError ( nameof ( request . PaymentMethods ) , "This field is required" ) ;
}
if ( ! ModelState . IsValid )
return this . CreateValidationError ( ModelState ) ;
var ppId = await _pullPaymentService . CreatePullPayment ( new HostedServices . CreatePullPayment ( )
{
StartsAt = request . StartsAt ,
ExpiresAt = request . ExpiresAt ,
Period = request . Period ,
2022-01-24 20:17:09 +09:00
BOLT11Expiration = request . BOLT11Expiration ,
2020-06-24 10:34:09 +09:00
Name = request . Name ,
2022-02-09 21:54:00 -08:00
Description = request . Description ,
2020-06-24 10:34:09 +09:00
Amount = request . Amount ,
2020-06-24 13:44:26 +09:00
Currency = request . Currency ,
2020-06-24 10:34:09 +09:00
StoreId = storeId ,
2022-04-28 02:51:04 +02:00
PaymentMethodIds = paymentMethods ,
AutoApproveClaims = request . AutoApproveClaims
2020-06-24 10:34:09 +09:00
} ) ;
2021-06-10 18:54:27 +09:00
var pp = await _pullPaymentService . GetPullPayment ( ppId , false ) ;
2020-06-24 10:34:09 +09:00
return this . Ok ( CreatePullPaymentData ( pp ) ) ;
}
private Client . Models . PullPaymentData CreatePullPaymentData ( Data . PullPaymentData pp )
{
var ppBlob = pp . GetBlob ( ) ;
return new BTCPayServer . Client . Models . PullPaymentData ( )
{
Id = pp . Id ,
StartsAt = pp . StartDate ,
ExpiresAt = pp . EndDate ,
Amount = ppBlob . Limit ,
Name = ppBlob . Name ,
2022-02-09 21:54:00 -08:00
Description = ppBlob . Description ,
2020-06-24 10:34:09 +09:00
Currency = ppBlob . Currency ,
Period = ppBlob . Period ,
Archived = pp . Archived ,
2022-04-28 02:51:04 +02:00
AutoApproveClaims = ppBlob . AutoApproveClaims ,
2022-01-24 20:17:09 +09:00
BOLT11Expiration = ppBlob . BOLT11Expiration ,
2020-06-24 10:34:09 +09:00
ViewLink = _linkGenerator . GetUriByAction (
2022-01-07 12:32:00 +09:00
nameof ( UIPullPaymentController . ViewPullPayment ) ,
"UIPullPayment" ,
2020-06-24 10:34:09 +09:00
new { pullPaymentId = pp . Id } ,
Request . Scheme ,
Request . Host ,
Request . PathBase )
} ;
}
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
[AllowAnonymous]
public async Task < IActionResult > GetPullPayment ( string pullPaymentId )
{
if ( pullPaymentId is null )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2021-06-10 18:54:27 +09:00
var pp = await _pullPaymentService . GetPullPayment ( pullPaymentId , false ) ;
2020-06-24 10:34:09 +09:00
if ( pp is null )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2020-06-24 10:34:09 +09:00
return Ok ( CreatePullPaymentData ( pp ) ) ;
}
2022-08-17 09:45:51 +02:00
private PayoutState [ ] ? GetStateFilter ( bool includeCancelled ) = >
includeCancelled
? null
: new [ ]
{
PayoutState . Completed , PayoutState . AwaitingApproval , PayoutState . AwaitingPayment ,
PayoutState . InProgress
} ;
2020-06-24 10:34:09 +09:00
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts")]
[AllowAnonymous]
public async Task < IActionResult > GetPayouts ( string pullPaymentId , bool includeCancelled = false )
{
if ( pullPaymentId is null )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2021-06-10 18:54:27 +09:00
var pp = await _pullPaymentService . GetPullPayment ( pullPaymentId , true ) ;
2020-06-24 10:34:09 +09:00
if ( pp is null )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2023-01-06 14:18:07 +01:00
var payouts = await _pullPaymentService . GetPayouts ( new PullPaymentHostedService . PayoutQuery ( )
2022-08-17 09:45:51 +02:00
{
2023-01-06 14:18:07 +01:00
PullPayments = new [ ] { pullPaymentId } ,
2022-08-17 09:45:51 +02:00
States = GetStateFilter ( includeCancelled )
} ) ;
2020-06-24 10:34:09 +09:00
return base . Ok ( payouts
2022-04-24 05:19:34 +02:00
. Select ( ToModel ) . ToList ( ) ) ;
2020-06-24 10:34:09 +09:00
}
2021-06-10 11:43:45 +02:00
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/payouts/{payoutId}")]
[AllowAnonymous]
public async Task < IActionResult > GetPayout ( string pullPaymentId , string payoutId )
{
if ( payoutId is null )
return PayoutNotFound ( ) ;
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2022-08-17 09:45:51 +02:00
var payout = ( await _pullPaymentService . GetPayouts ( new PullPaymentHostedService . PayoutQuery ( )
{
2023-01-06 14:18:07 +01:00
PullPayments = new [ ] { pullPaymentId } ,
PayoutIds = new [ ] { payoutId }
2022-08-17 09:45:51 +02:00
} ) ) . FirstOrDefault ( ) ;
2023-01-06 14:18:07 +01:00
2021-12-31 16:59:02 +09:00
if ( payout is null )
2021-06-10 11:43:45 +02:00
return PayoutNotFound ( ) ;
2022-04-24 05:19:34 +02:00
return base . Ok ( ToModel ( payout ) ) ;
2021-06-10 11:43:45 +02:00
}
2023-01-25 21:43:07 -08:00
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}/lnurl")]
[AllowAnonymous]
public async Task < IActionResult > GetPullPaymentLNURL ( string pullPaymentId )
{
var pp = await _pullPaymentService . GetPullPayment ( pullPaymentId , false ) ;
if ( pp is null )
return PullPaymentNotFound ( ) ;
var blob = pp . GetBlob ( ) ;
var pms = blob . SupportedPaymentMethods . FirstOrDefault ( id = > id . PaymentType = = LightningPaymentType . Instance & & _networkProvider . DefaultNetwork . CryptoCode = = id . CryptoCode ) ;
if ( pms is not null & & blob . Currency . Equals ( pms . CryptoCode , StringComparison . InvariantCultureIgnoreCase ) )
{
var lnurlEndpoint = new Uri ( Url . Action ( "GetLNURLForPullPayment" , "UILNURL" , new
{
cryptoCode = _networkProvider . DefaultNetwork . CryptoCode ,
pullPaymentId = pullPaymentId
} , Request . Scheme , Request . Host . ToString ( ) ) ) ;
return base . Ok ( new PullPaymentLNURL ( ) {
LNURLBech32 = LNURL . LNURL . EncodeUri ( lnurlEndpoint , "withdrawRequest" , true ) . ToString ( ) ,
LNURLUri = LNURL . LNURL . EncodeUri ( lnurlEndpoint , "withdrawRequest" , false ) . ToString ( )
} ) ;
}
return this . CreateAPIError ( "lnurl-not-supported" , "LNURL not supported for this pull payment" ) ;
}
2022-04-24 05:19:34 +02:00
private Client . Models . PayoutData ToModel ( Data . PayoutData p )
2020-06-24 10:34:09 +09:00
{
var blob = p . GetBlob ( _serializerSettings ) ;
var model = new Client . Models . PayoutData ( )
{
Id = p . Id ,
PullPaymentId = p . PullPaymentDataId ,
Date = p . Date ,
Amount = blob . Amount ,
PaymentMethodAmount = blob . CryptoAmount ,
2020-06-24 13:44:26 +09:00
Revision = blob . Revision ,
2021-04-13 10:36:49 +02:00
State = p . State
2020-06-24 10:34:09 +09:00
} ;
2021-04-13 10:36:49 +02:00
model . Destination = blob . Destination ;
2020-06-24 10:34:09 +09:00
model . PaymentMethod = p . PaymentMethodId ;
2021-11-14 21:25:59 -08:00
model . CryptoCode = p . GetPaymentMethodId ( ) . CryptoCode ;
2022-11-15 10:40:57 +01:00
model . PaymentProof = p . GetProofBlobJson ( ) ;
2020-06-24 10:34:09 +09:00
return model ;
}
[HttpPost("~/api/v1/pull-payments/{pullPaymentId}/payouts")]
[AllowAnonymous]
2022-11-22 20:17:29 +09:00
public async Task < IActionResult > CreatePayout ( string pullPaymentId , CreatePayoutRequest request , CancellationToken cancellationToken )
2020-06-24 10:34:09 +09:00
{
2021-04-13 10:36:49 +02:00
if ( ! PaymentMethodId . TryParse ( request ? . PaymentMethod , out var paymentMethodId ) )
{
ModelState . AddModelError ( nameof ( request . PaymentMethod ) , "Invalid payment method" ) ;
return this . CreateValidationError ( ModelState ) ;
}
2021-12-31 16:59:02 +09:00
2021-10-18 15:00:38 +09:00
var payoutHandler = _payoutHandlers . FindPayoutHandler ( paymentMethodId ) ;
2021-04-13 10:36:49 +02:00
if ( payoutHandler is null )
2020-06-24 10:34:09 +09:00
{
ModelState . AddModelError ( nameof ( request . PaymentMethod ) , "Invalid payment method" ) ;
return this . CreateValidationError ( ModelState ) ;
}
2021-06-10 11:43:45 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2020-06-24 10:34:09 +09:00
var pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp is null )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2020-06-24 10:34:09 +09:00
var ppBlob = pp . GetBlob ( ) ;
2022-11-22 20:17:29 +09:00
var destination = await payoutHandler . ParseAndValidateClaimDestination ( paymentMethodId , request ! . Destination , ppBlob , cancellationToken ) ;
2021-10-18 05:37:59 +02:00
if ( destination . destination is null )
2020-06-24 10:34:09 +09:00
{
2021-12-31 16:59:02 +09:00
ModelState . AddModelError ( nameof ( request . Destination ) , destination . error ? ? "The destination is invalid for the payment specified" ) ;
2020-06-24 10:34:09 +09:00
return this . CreateValidationError ( ModelState ) ;
}
2021-10-18 05:37:59 +02:00
if ( request . Amount is null & & destination . destination . Amount ! = null )
{
request . Amount = destination . destination . Amount ;
}
else if ( request . Amount ! = null & & destination . destination . Amount ! = null & & request . Amount ! = destination . destination . Amount )
{
ModelState . AddModelError ( nameof ( request . Amount ) , $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})" ) ;
return this . CreateValidationError ( ModelState ) ;
}
if ( request . Amount is { } v & & ( v < ppBlob . MinimumClaim | | v = = 0.0 m ) )
2020-06-24 10:34:09 +09:00
{
ModelState . AddModelError ( nameof ( request . Amount ) , $"Amount too small (should be at least {ppBlob.MinimumClaim})" ) ;
return this . CreateValidationError ( ModelState ) ;
}
2023-01-06 14:18:07 +01:00
var result = await _pullPaymentService . Claim ( new ClaimRequest ( )
2020-06-24 10:34:09 +09:00
{
2021-10-18 05:37:59 +02:00
Destination = destination . destination ,
2020-06-24 10:34:09 +09:00
PullPaymentId = pullPaymentId ,
Value = request . Amount ,
2022-04-24 05:19:34 +02:00
PaymentMethodId = paymentMethodId ,
2020-06-24 10:34:09 +09:00
} ) ;
2023-01-06 14:18:07 +01:00
return HandleClaimResult ( result ) ;
2022-04-24 05:19:34 +02:00
}
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
[HttpPost("~/api/v1/stores/{storeId}/payouts")]
2023-01-26 01:46:05 +01:00
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
2022-04-24 05:19:34 +02:00
public async Task < IActionResult > CreatePayoutThroughStore ( string storeId , CreatePayoutThroughStoreRequest request )
{
2023-01-26 01:46:05 +01:00
if ( request . Approved is true )
{
if ( ! ( await _authorizationService . AuthorizeAsync ( User , null ,
new PolicyRequirement ( Policies . CanCreatePullPayments ) ) ) . Succeeded )
{
return this . CreateAPIPermissionError ( Policies . CanCreatePullPayments ) ;
}
}
2022-04-24 05:19:34 +02:00
if ( request is null | | ! PaymentMethodId . TryParse ( request ? . PaymentMethod , out var paymentMethodId ) )
{
ModelState . AddModelError ( nameof ( request . PaymentMethod ) , "Invalid payment method" ) ;
return this . CreateValidationError ( ModelState ) ;
}
var payoutHandler = _payoutHandlers . FindPayoutHandler ( paymentMethodId ) ;
if ( payoutHandler is null )
{
ModelState . AddModelError ( nameof ( request . PaymentMethod ) , "Invalid payment method" ) ;
return this . CreateValidationError ( ModelState ) ;
}
await using var ctx = _dbContextFactory . CreateContext ( ) ;
PullPaymentBlob ? ppBlob = null ;
if ( request ? . PullPaymentId is not null )
{
var pp = await ctx . PullPayments . FirstOrDefaultAsync ( data = >
data . Id = = request . PullPaymentId & & data . StoreId = = storeId ) ;
if ( pp is null )
return PullPaymentNotFound ( ) ;
ppBlob = pp . GetBlob ( ) ;
}
2022-11-22 20:17:29 +09:00
var destination = await payoutHandler . ParseAndValidateClaimDestination ( paymentMethodId , request ! . Destination , ppBlob , default ) ;
2022-04-24 05:19:34 +02:00
if ( destination . destination is null )
{
ModelState . AddModelError ( nameof ( request . Destination ) , destination . error ? ? "The destination is invalid for the payment specified" ) ;
return this . CreateValidationError ( ModelState ) ;
}
if ( request . Amount is null & & destination . destination . Amount ! = null )
{
request . Amount = destination . destination . Amount ;
}
else if ( request . Amount ! = null & & destination . destination . Amount ! = null & & request . Amount ! = destination . destination . Amount )
{
ModelState . AddModelError ( nameof ( request . Amount ) , $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})" ) ;
return this . CreateValidationError ( ModelState ) ;
}
if ( request . Amount is { } v & & ( v < ppBlob ? . MinimumClaim | | v = = 0.0 m ) )
{
var minimumClaim = ppBlob ? . MinimumClaim is decimal val ? val : 0.0 m ;
ModelState . AddModelError ( nameof ( request . Amount ) , $"Amount too small (should be at least {minimumClaim})" ) ;
return this . CreateValidationError ( ModelState ) ;
}
var result = await _pullPaymentService . Claim ( new ClaimRequest ( )
{
Destination = destination . destination ,
PullPaymentId = request . PullPaymentId ,
PreApprove = request . Approved ,
Value = request . Amount ,
PaymentMethodId = paymentMethodId ,
StoreId = storeId
} ) ;
return HandleClaimResult ( result ) ;
}
private IActionResult HandleClaimResult ( ClaimRequest . ClaimResponse result )
{
2020-06-24 10:34:09 +09:00
switch ( result . Result )
{
case ClaimRequest . ClaimResult . Ok :
break ;
case ClaimRequest . ClaimResult . Duplicate :
return this . CreateAPIError ( "duplicate-destination" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . Expired :
return this . CreateAPIError ( "expired" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . NotStarted :
return this . CreateAPIError ( "not-started" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . Archived :
return this . CreateAPIError ( "archived" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . Overdraft :
return this . CreateAPIError ( "overdraft" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . AmountTooLow :
return this . CreateAPIError ( "amount-too-low" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
case ClaimRequest . ClaimResult . PaymentMethodNotSupported :
return this . CreateAPIError ( "payment-method-not-supported" , ClaimRequest . GetErrorMessage ( result . Result ) ) ;
default :
throw new NotSupportedException ( "Unsupported ClaimResult" ) ;
}
2022-04-24 05:19:34 +02:00
return Ok ( ToModel ( result . PayoutData ) ) ;
2020-06-24 10:34:09 +09:00
}
[HttpDelete("~/api/v1/stores/{storeId}/pull-payments/{pullPaymentId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > ArchivePullPayment ( string storeId , string pullPaymentId )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
var pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp is null | | pp . StoreId ! = storeId )
2021-06-10 11:43:45 +02:00
return PullPaymentNotFound ( ) ;
2020-06-24 10:34:09 +09:00
await _pullPaymentService . Cancel ( new PullPaymentHostedService . CancelRequest ( pullPaymentId ) ) ;
return Ok ( ) ;
}
2022-04-24 05:19:34 +02:00
[HttpGet("~/api/v1/stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > GetStorePayouts ( string storeId , bool includeCancelled = false )
{
2022-08-17 09:45:51 +02:00
var payouts = await _pullPaymentService . GetPayouts ( new PullPaymentHostedService . PayoutQuery ( )
{
2023-01-06 14:18:07 +01:00
Stores = new [ ] { storeId } ,
2022-08-17 09:45:51 +02:00
States = GetStateFilter ( includeCancelled )
} ) ;
2023-01-06 14:18:07 +01:00
2022-04-24 05:19:34 +02:00
return base . Ok ( payouts
2022-11-15 10:40:57 +01:00
. Select ( ToModel ) . ToArray ( ) ) ;
2022-04-24 05:19:34 +02:00
}
2020-06-24 10:34:09 +09:00
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > CancelPayout ( string storeId , string payoutId )
{
2023-01-06 14:18:07 +01:00
var res = await _pullPaymentService . Cancel ( new PullPaymentHostedService . CancelRequest ( new [ ] { payoutId } , new [ ] { storeId } ) ) ;
2022-11-15 10:40:57 +01:00
return MapResult ( res . First ( ) . Value ) ;
2020-06-24 10:34:09 +09:00
}
2020-06-24 13:44:26 +09:00
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > ApprovePayout ( string storeId , string payoutId , ApprovePayoutRequest approvePayoutRequest , CancellationToken cancellationToken = default )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
ctx . ChangeTracker . QueryTrackingBehavior = QueryTrackingBehavior . NoTracking ;
var revision = approvePayoutRequest ? . Revision ;
if ( revision is null )
{
ModelState . AddModelError ( nameof ( approvePayoutRequest . Revision ) , "The `revision` property is required" ) ;
}
if ( ! ModelState . IsValid )
return this . CreateValidationError ( ModelState ) ;
var payout = await ctx . Payouts . GetPayout ( payoutId , storeId , true , true ) ;
if ( payout is null )
2021-06-10 11:43:45 +02:00
return PayoutNotFound ( ) ;
2021-11-14 21:25:59 -08:00
RateResult ? rateResult = null ;
2020-06-24 13:44:26 +09:00
try
{
rateResult = await _pullPaymentService . GetRate ( payout , approvePayoutRequest ? . RateRule , cancellationToken ) ;
if ( rateResult . BidAsk = = null )
{
return this . CreateAPIError ( "rate-unavailable" , $"Rate unavailable: {rateResult.EvaluatedRule}" ) ;
}
}
catch ( FormatException )
{
ModelState . AddModelError ( nameof ( approvePayoutRequest . RateRule ) , "Invalid RateRule" ) ;
return this . CreateValidationError ( ModelState ) ;
}
2022-12-04 13:23:59 +01:00
var result = ( await _pullPaymentService . Approve ( new PullPaymentHostedService . PayoutApproval ( )
2020-06-24 13:44:26 +09:00
{
PayoutId = payoutId ,
2021-11-14 21:25:59 -08:00
Revision = revision ! . Value ,
2020-06-24 13:44:26 +09:00
Rate = rateResult . BidAsk . Ask
2022-12-04 13:23:59 +01:00
} ) ) . Result ;
2020-06-24 13:44:26 +09:00
var errorMessage = PullPaymentHostedService . PayoutApproval . GetErrorMessage ( result ) ;
switch ( result )
{
case PullPaymentHostedService . PayoutApproval . Result . Ok :
2022-04-24 05:19:34 +02:00
return Ok ( ToModel ( await ctx . Payouts . GetPayout ( payoutId , storeId , true ) ) ) ;
2020-06-24 13:44:26 +09:00
case PullPaymentHostedService . PayoutApproval . Result . InvalidState :
return this . CreateAPIError ( "invalid-state" , errorMessage ) ;
case PullPaymentHostedService . PayoutApproval . Result . TooLowAmount :
return this . CreateAPIError ( "amount-too-low" , errorMessage ) ;
case PullPaymentHostedService . PayoutApproval . Result . OldRevision :
return this . CreateAPIError ( "old-revision" , errorMessage ) ;
case PullPaymentHostedService . PayoutApproval . Result . NotFound :
2021-06-10 11:43:45 +02:00
return PayoutNotFound ( ) ;
2020-06-24 13:44:26 +09:00
default :
throw new NotSupportedException ( ) ;
}
}
2021-06-10 11:43:45 +02:00
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}/mark-paid")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > MarkPayoutPaid ( string storeId , string payoutId , CancellationToken cancellationToken = default )
{
2022-11-15 10:40:57 +01:00
return await MarkPayout ( storeId , payoutId , new Client . Models . MarkPayoutRequest ( )
{
State = PayoutState . Completed ,
PaymentProof = null
} ) ;
}
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}/mark")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > MarkPayout ( string storeId , string payoutId , Client . Models . MarkPayoutRequest request )
{
request ? ? = new ( ) ;
if ( request . State = = PayoutState . Cancelled )
{
return await CancelPayout ( storeId , payoutId ) ;
}
if ( request . PaymentProof is not null & &
! BitcoinLikePayoutHandler . TryParseProofType ( request . PaymentProof , out string _ ) )
{
ModelState . AddModelError ( nameof ( request . PaymentProof ) , "Payment proof must have a 'proofType' property" ) ;
}
2021-06-10 11:43:45 +02:00
if ( ! ModelState . IsValid )
return this . CreateValidationError ( ModelState ) ;
2022-11-15 10:40:57 +01:00
var result = await _pullPaymentService . MarkPaid ( new MarkPayoutRequest ( )
2021-06-10 11:43:45 +02:00
{
2022-11-15 10:40:57 +01:00
Proof = request . PaymentProof ,
PayoutId = payoutId ,
State = request . State
2021-06-10 11:43:45 +02:00
} ) ;
2022-11-15 10:40:57 +01:00
return MapResult ( result ) ;
2021-06-10 11:43:45 +02:00
}
2021-12-31 16:59:02 +09:00
2022-11-15 10:40:57 +01:00
[HttpGet("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task < IActionResult > GetStorePayout ( string storeId , string payoutId )
{
await using var ctx = _dbContextFactory . CreateContext ( ) ;
var payout = ( await _pullPaymentService . GetPayouts ( new PullPaymentHostedService . PayoutQuery ( )
{
2023-01-06 14:18:07 +01:00
Stores = new [ ] { storeId } ,
PayoutIds = new [ ] { payoutId }
2022-11-15 10:40:57 +01:00
} ) ) . FirstOrDefault ( ) ;
if ( payout is null )
return PayoutNotFound ( ) ;
return base . Ok ( ToModel ( payout ) ) ;
}
2023-01-06 14:18:07 +01:00
2022-11-15 10:40:57 +01:00
private IActionResult MapResult ( MarkPayoutRequest . PayoutPaidResult result )
{
var errorMessage = MarkPayoutRequest . GetErrorMessage ( result ) ;
return result switch
{
MarkPayoutRequest . PayoutPaidResult . Ok = > Ok ( ) ,
MarkPayoutRequest . PayoutPaidResult . InvalidState = > this . CreateAPIError ( "invalid-state" , errorMessage ) ,
MarkPayoutRequest . PayoutPaidResult . NotFound = > PayoutNotFound ( ) ,
_ = > throw new NotSupportedException ( )
} ;
}
2021-06-10 11:43:45 +02:00
private IActionResult PayoutNotFound ( )
{
return this . CreateAPIError ( 404 , "payout-not-found" , "The payout was not found" ) ;
}
private IActionResult PullPaymentNotFound ( )
{
return this . CreateAPIError ( 404 , "pullpayment-not-found" , "The pull payment was not found" ) ;
}
2020-06-24 10:34:09 +09:00
}
}