Greenfield: Allow marking payout status and payment proofs (#4244)

This allows external services to integrate with the payouts system to process payouts. This is also  a step to allow plugins to provide payout processors.

* It provides the payment proof through the greenfield payoust api.
* It allows you to set the state of a payout outside of the usual flow:
  * When state is awaiting payment, allow setting to In progess or completed
  * When state is in progress, allow setting back to awaiting payment
This commit is contained in:
Andrew Camilleri
2022-11-15 10:40:57 +01:00
committed by GitHub
parent 17f3b4125b
commit 2d23819944
16 changed files with 500 additions and 91 deletions

View File

@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
namespace BTCPayServer.Controllers.Greenfield
{
@@ -243,6 +244,7 @@ namespace BTCPayServer.Controllers.Greenfield
model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId;
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
model.PaymentProof = p.GetProofBlobJson();
return model;
}
@@ -417,19 +419,15 @@ namespace BTCPayServer.Controllers.Greenfield
return base.Ok(payouts
.Select(ToModel).ToList());
.Select(ToModel).ToArray());
}
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CancelPayout(string storeId, string payoutId)
{
using var ctx = _dbContextFactory.CreateContext();
var payout = await ctx.Payouts.GetPayout(payoutId, storeId);
if (payout is null)
return PayoutNotFound();
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }));
return Ok();
var res= await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }, new []{storeId}));
return MapResult(res.First().Value);
}
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
@@ -490,29 +488,67 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> MarkPayoutPaid(string storeId, string payoutId, CancellationToken cancellationToken = default)
{
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");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var result = await _pullPaymentService.MarkPaid(new PayoutPaidRequest()
var result = await _pullPaymentService.MarkPaid(new MarkPayoutRequest()
{
//TODO: Allow API to specify the manual proof object
Proof = null,
PayoutId = payoutId
Proof = request.PaymentProof,
PayoutId = payoutId,
State = request.State
});
var errorMessage = PayoutPaidRequest.GetErrorMessage(result);
switch (result)
{
case PayoutPaidRequest.PayoutPaidResult.Ok:
return Ok();
case PayoutPaidRequest.PayoutPaidResult.InvalidState:
return this.CreateAPIError("invalid-state", errorMessage);
case PayoutPaidRequest.PayoutPaidResult.NotFound:
return PayoutNotFound();
default:
throw new NotSupportedException();
}
return MapResult(result);
}
[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()
{
Stores = new[] {storeId}, PayoutIds = new[] {payoutId}
})).FirstOrDefault();
if (payout is null)
return PayoutNotFound();
return base.Ok(ToModel(payout));
}
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()
};
}
private IActionResult PayoutNotFound()
{
return this.CreateAPIError(404, "payout-not-found", "The payout was not found");