mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Mark Payouts as Paid (#2539)
* Mark Payouts as Paid This PR allows users to mark payouts as paid manually through the UI and through the API. It also sets up the payout proof system to be able store a manual proof that will in a later PR allow you to specify a proof of payment (link or text) * add docs, test and greenfield client * remove extra docs stuff * Update BTCPayServer.Tests/GreenfieldAPITests.cs Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com> * clean up pull payment/payouts fetch code * Ensure payoutis are retrieved with pull payment Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
This commit is contained in:
parent
f1f3dffc97
commit
cd9feccf6e
16 changed files with 320 additions and 29 deletions
|
@ -57,5 +57,15 @@ namespace BTCPayServer.Client
|
|||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}", bodyPayload: request, method: HttpMethod.Post), cancellationToken);
|
||||
return await HandleResponse<PayoutData>(response);
|
||||
}
|
||||
|
||||
public async Task MarkPayoutPaid(string storeId, string payoutId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest(
|
||||
$"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}/mark-paid",
|
||||
method: HttpMethod.Post), cancellationToken);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -529,6 +529,11 @@ namespace BTCPayServer.Tests
|
|||
// The payout should round the value of the payment down to the network of the payment method
|
||||
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
|
||||
Assert.Equal(12.303228134m, payout.Amount);
|
||||
|
||||
await client.MarkPayoutPaid(storeId, payout.Id);
|
||||
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
|
||||
Assert.Equal(PayoutState.Completed, payout.State);
|
||||
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -164,10 +164,10 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
public async Task<IActionResult> GetPullPayment(string pullPaymentId)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return NotFound();
|
||||
return PullPaymentNotFound();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
return PullPaymentNotFound();
|
||||
return Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
|
||||
|
@ -176,19 +176,33 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
public async Task<IActionResult> GetPayouts(string pullPaymentId, bool includeCancelled = false)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return NotFound();
|
||||
return PullPaymentNotFound();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var payouts = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId)
|
||||
.Where(p => p.State != PayoutState.Cancelled || includeCancelled)
|
||||
.ToListAsync();
|
||||
return PullPaymentNotFound();
|
||||
var payouts = pp.Payouts .Where(p => p.State != PayoutState.Cancelled || includeCancelled).ToList();
|
||||
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
|
||||
return base.Ok(payouts
|
||||
.Select(p => ToModel(p, cd)).ToList());
|
||||
}
|
||||
|
||||
[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();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var payout = pp.Payouts.FirstOrDefault(p => p.Id == payoutId);
|
||||
if(payout is null )
|
||||
return PayoutNotFound();
|
||||
var cd = _currencyNameTable.GetCurrencyData(payout.PullPaymentData.GetBlob().Currency, false);
|
||||
return base.Ok(ToModel(payout, cd));
|
||||
}
|
||||
|
||||
private Client.Models.PayoutData ToModel(Data.PayoutData p, CurrencyData cd)
|
||||
{
|
||||
var blob = p.GetBlob(_serializerSettings);
|
||||
|
@ -226,10 +240,10 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
return PullPaymentNotFound();
|
||||
var ppBlob = pp.GetBlob();
|
||||
IClaimDestination destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination);
|
||||
if (destination is null)
|
||||
|
@ -282,7 +296,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
if (pp is null || pp.StoreId != storeId)
|
||||
return NotFound();
|
||||
return PullPaymentNotFound();
|
||||
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(pullPaymentId));
|
||||
return Ok();
|
||||
}
|
||||
|
@ -294,7 +308,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var payout = await ctx.Payouts.GetPayout(payoutId, storeId);
|
||||
if (payout is null)
|
||||
return NotFound();
|
||||
return PayoutNotFound();
|
||||
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }));
|
||||
return Ok();
|
||||
}
|
||||
|
@ -314,7 +328,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
return this.CreateValidationError(ModelState);
|
||||
var payout = await ctx.Payouts.GetPayout(payoutId, storeId, true, true);
|
||||
if (payout is null)
|
||||
return NotFound();
|
||||
return PayoutNotFound();
|
||||
RateResult rateResult = null;
|
||||
try
|
||||
{
|
||||
|
@ -349,10 +363,46 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
case PullPaymentHostedService.PayoutApproval.Result.OldRevision:
|
||||
return this.CreateAPIError("old-revision", errorMessage);
|
||||
case PullPaymentHostedService.PayoutApproval.Result.NotFound:
|
||||
return NotFound();
|
||||
return PayoutNotFound();
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var result = await _pullPaymentService.MarkPaid(new PayoutPaidRequest()
|
||||
{
|
||||
//TODO: Allow API to specify the manual proof object
|
||||
Proof = null,
|
||||
PayoutId = payoutId
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ namespace BTCPayServer.Controllers
|
|||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
}
|
||||
|
||||
[Route("pull-payments/{pullPaymentId}")]
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
|
||||
{
|
||||
|
|
|
@ -295,6 +295,44 @@ namespace BTCPayServer.Controllers
|
|||
return RedirectToAction(nameof(WalletSend), new {walletId, bip21});
|
||||
}
|
||||
|
||||
case "mark-paid":
|
||||
{
|
||||
await using var ctx = this._dbContextFactory.CreateContext();
|
||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
||||
for (int i = 0; i < payouts.Count; i++)
|
||||
{
|
||||
var payout = payouts[i];
|
||||
if (payout.State != PayoutState.AwaitingPayment)
|
||||
continue;
|
||||
|
||||
var result = await _pullPaymentService.MarkPaid(new PayoutPaidRequest()
|
||||
{
|
||||
PayoutId = payout.Id
|
||||
});
|
||||
if (result != PayoutPaidRequest.PayoutPaidResult.Ok)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PayoutPaidRequest.GetErrorMessage(result),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Payouts marked as paid", Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts),
|
||||
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
|
||||
}
|
||||
|
||||
case "cancel":
|
||||
await _pullPaymentService.Cancel(
|
||||
new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
|
||||
|
@ -376,7 +414,7 @@ namespace BTCPayServer.Controllers
|
|||
var handler = _payoutHandlers
|
||||
.FirstOrDefault(handler => handler.CanHandle(item.Payout.GetPaymentMethodId()));
|
||||
var proofBlob = handler?.ParseProof(item.Payout);
|
||||
m.TransactionLink = proofBlob?.Link;
|
||||
m.ProofLink = proofBlob?.Link;
|
||||
state.Payouts.Add(m);
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ using NBitcoin.Payment;
|
|||
using NBitcoin.RPC;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NewBlockEvent = BTCPayServer.Events.NewBlockEvent;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
|
||||
|
@ -70,8 +71,16 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
if (payout?.Proof is null)
|
||||
return null;
|
||||
var paymentMethodId = payout.GetPaymentMethodId();
|
||||
var res = JsonConvert.DeserializeObject<PayoutTransactionOnChainBlob>(Encoding.UTF8.GetString(payout.Proof), _jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode));
|
||||
var raw = JObject.Parse(Encoding.UTF8.GetString(payout.Proof));
|
||||
if (raw.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType) &&
|
||||
proofType.Value<string>() == ManualPayoutProof.Type)
|
||||
{
|
||||
return raw.ToObject<ManualPayoutProof>();
|
||||
}
|
||||
var res = raw.ToObject<PayoutTransactionOnChainBlob>(
|
||||
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode)));
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||
if (res == null) return null;
|
||||
res.LinkTemplate = network.BlockExplorerLink;
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ namespace BTCPayServer.Data
|
|||
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
|
||||
|
||||
[JsonIgnore] public string LinkTemplate { get; set; }
|
||||
public string ProofType { get; } = "PayoutTransactionOnChainBlob";
|
||||
|
||||
[JsonIgnore]
|
||||
public string Link
|
||||
{
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
using System;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public interface IClaimDestination
|
||||
{
|
||||
}
|
||||
|
||||
public interface IPayoutProof
|
||||
{
|
||||
string Link { get; }
|
||||
string Id { get; }
|
||||
}
|
||||
}
|
||||
|
|
9
BTCPayServer/Data/Payouts/IPayoutProof.cs
Normal file
9
BTCPayServer/Data/Payouts/IPayoutProof.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace BTCPayServer.Data
|
||||
{
|
||||
public interface IPayoutProof
|
||||
{
|
||||
string ProofType { get; }
|
||||
string Link { get; }
|
||||
string Id { get; }
|
||||
}
|
||||
}
|
10
BTCPayServer/Data/Payouts/ManualPayoutProof.cs
Normal file
10
BTCPayServer/Data/Payouts/ManualPayoutProof.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class ManualPayoutProof : IPayoutProof
|
||||
{
|
||||
public static string Type = "ManualPayoutProof";
|
||||
public string ProofType { get; } = Type;
|
||||
public string Link { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -38,5 +38,17 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
|
||||
}
|
||||
|
||||
public static void SetProofBlob(this PayoutData data, ManualPayoutProof blob)
|
||||
{
|
||||
if(blob is null)
|
||||
return;
|
||||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
|
||||
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
|
||||
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
|
||||
{
|
||||
data.Proof = bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,8 +123,8 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
public async Task<Data.PullPaymentData> GetPullPayment(string pullPaymentId)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
return await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
return await ctx.PullPayments.Include(data => data.Payouts).FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
||||
}
|
||||
|
||||
class PayoutRequest
|
||||
|
@ -206,6 +206,10 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
await HandleCancel(cancel);
|
||||
}
|
||||
if (o is InternalPayoutPaidRequest paid)
|
||||
{
|
||||
await HandleMarkPaid(paid);
|
||||
}
|
||||
foreach (IPayoutHandler payoutHandler in _payoutHandlers)
|
||||
{
|
||||
await payoutHandler.BackgroundCheck(o);
|
||||
|
@ -291,6 +295,35 @@ namespace BTCPayServer.HostedServices
|
|||
req.Completion.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
private async Task HandleMarkPaid(InternalPayoutPaidRequest req)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.Request.PayoutId).FirstOrDefaultAsync();
|
||||
if (payout is null)
|
||||
{
|
||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.NotFound);
|
||||
return;
|
||||
}
|
||||
if (payout.State != PayoutState.AwaitingPayment)
|
||||
{
|
||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.InvalidState);
|
||||
return;
|
||||
}
|
||||
if (req.Request.Proof != null)
|
||||
{
|
||||
payout.SetProofBlob(req.Request.Proof);
|
||||
}
|
||||
payout.State = PayoutState.Completed;
|
||||
await ctx.SaveChangesAsync();
|
||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.Ok);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
req.Completion.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCreatePayout(PayoutRequest req)
|
||||
{
|
||||
|
@ -444,6 +477,60 @@ namespace BTCPayServer.HostedServices
|
|||
_subscriptions.Dispose();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PayoutPaidRequest.PayoutPaidResult> MarkPaid(PayoutPaidRequest request)
|
||||
{
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
var cts = new TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (!_Channel.Writer.TryWrite(new InternalPayoutPaidRequest(cts, request)))
|
||||
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
||||
return cts.Task;
|
||||
}
|
||||
|
||||
|
||||
class InternalPayoutPaidRequest
|
||||
{
|
||||
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource, PayoutPaidRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (completionSource == null)
|
||||
throw new ArgumentNullException(nameof(completionSource));
|
||||
Completion = completionSource;
|
||||
Request = request;
|
||||
}
|
||||
public TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> Completion { get; set; }
|
||||
public PayoutPaidRequest Request { get; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PayoutPaidRequest
|
||||
{
|
||||
public enum PayoutPaidResult
|
||||
{
|
||||
Ok,
|
||||
NotFound,
|
||||
InvalidState
|
||||
}
|
||||
public string PayoutId { get; set; }
|
||||
public ManualPayoutProof Proof { get; set; }
|
||||
|
||||
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:
|
||||
return "The payout is not in a state that can be marked as paid";
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ClaimRequest
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public string PullPaymentName { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string Amount { get; set; }
|
||||
public string TransactionLink { get; set; }
|
||||
public string ProofLink { get; set; }
|
||||
}
|
||||
|
||||
public class PayoutStateSet
|
||||
|
|
|
@ -139,7 +139,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5">
|
||||
<h2 class="h4 mb-3">Awaiting Claims</h2>
|
||||
<h2 class="h4 mb-3">Claims</h2>
|
||||
<div class="table-responsive">
|
||||
@if (Model.Payouts.Any())
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, "Manage payouts", Context.GetStoreData().StoreName);
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Payouts, $"Manage {Model.PaymentMethodId.ToPrettyString()} payouts", Context.GetStoreData().StoreName);
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
|
@ -55,6 +55,7 @@
|
|||
case PayoutState.AwaitingPayment:
|
||||
stateActions.Add(("pay", "Send selected payouts"));
|
||||
stateActions.Add(("cancel", "Cancel selected payouts"));
|
||||
stateActions.Add(("mark-paid", "Mark selected payouts as already paid"));
|
||||
break;
|
||||
}
|
||||
<div class="tab-pane @(index == 0 ? "active" : "") " id="@state.State" role="tabpanel">
|
||||
|
@ -122,9 +123,9 @@
|
|||
@if (state.State != PayoutState.AwaitingApproval)
|
||||
{
|
||||
<td class="text-end">
|
||||
@if (!(pp.TransactionLink is null))
|
||||
@if (!(pp.ProofLink is null))
|
||||
{
|
||||
<a class="transaction-link" href="@pp.TransactionLink">Link</a>
|
||||
<a class="transaction-link" href="@pp.ProofLink">Link</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
|
|
|
@ -423,6 +423,67 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payouts/{payoutId}/mark-paid": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The ID of the store",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "payoutId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The ID of the payout",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
|
||||
"summary": "Mark Payout as Paid",
|
||||
"operationId": "PullPayments_MarkPayoutPaid",
|
||||
"description": "Mark a payout as paid",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The payout has been marked paid, transitioning to `Completed` state."
|
||||
},
|
||||
"422": {
|
||||
"description": "Unable to validate the request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Wellknown error codes are: `invalid-state`",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The payout is not found"
|
||||
}
|
||||
},
|
||||
"tags": [ "Pull payments (Management)" ],
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canmanagepullpayments"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
|
Loading…
Add table
Reference in a new issue