From cd9feccf6e96bbebfa24a71583d792dcd9da741a Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 10 Jun 2021 11:43:45 +0200 Subject: [PATCH] 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> --- .../BTCPayServerClient.PullPayments.cs | 10 ++ BTCPayServer.Tests/GreenfieldAPITests.cs | 5 + .../GreenField/PullPaymentController.cs | 78 +++++++++++++--- .../Controllers/PullPaymentController.cs | 1 + .../WalletsController.PullPayments.cs | 40 +++++++- .../BitcoinLike/BitcoinLikePayoutHandler.cs | 11 ++- .../PayoutTransactionOnChainBlob.cs | 2 + .../Data/Payouts/IClaimDestination.cs | 8 +- BTCPayServer/Data/Payouts/IPayoutProof.cs | 9 ++ .../Data/Payouts/ManualPayoutProof.cs | 10 ++ BTCPayServer/Data/Payouts/PayoutExtensions.cs | 12 +++ .../PullPaymentHostedService.cs | 91 ++++++++++++++++++- .../Models/WalletViewModels/PayoutsModel.cs | 2 +- .../Views/PullPayment/ViewPullPayment.cshtml | 2 +- BTCPayServer/Views/Wallets/Payouts.cshtml | 7 +- .../v1/swagger.template.pull-payments.json | 61 +++++++++++++ 16 files changed, 320 insertions(+), 29 deletions(-) create mode 100644 BTCPayServer/Data/Payouts/IPayoutProof.cs create mode 100644 BTCPayServer/Data/Payouts/ManualPayoutProof.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs b/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs index 294c5a120..01a8f03e7 100644 --- a/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs +++ b/BTCPayServer.Client/BTCPayServerClient.PullPayments.cs @@ -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(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); + } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index acd0d84d2..3875172e2 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -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)); } } diff --git a/BTCPayServer/Controllers/GreenField/PullPaymentController.cs b/BTCPayServer/Controllers/GreenField/PullPaymentController.cs index f75b0878d..14d2e63f1 100644 --- a/BTCPayServer/Controllers/GreenField/PullPaymentController.cs +++ b/BTCPayServer/Controllers/GreenField/PullPaymentController.cs @@ -164,10 +164,10 @@ namespace BTCPayServer.Controllers.GreenField public async Task 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 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 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 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"); + } } } diff --git a/BTCPayServer/Controllers/PullPaymentController.cs b/BTCPayServer/Controllers/PullPaymentController.cs index b633d71cd..c78f1c0f8 100644 --- a/BTCPayServer/Controllers/PullPaymentController.cs +++ b/BTCPayServer/Controllers/PullPaymentController.cs @@ -42,6 +42,7 @@ namespace BTCPayServer.Controllers _serializerSettings = serializerSettings; _payoutHandlers = payoutHandlers; } + [Route("pull-payments/{pullPaymentId}")] public async Task ViewPullPayment(string pullPaymentId) { diff --git a/BTCPayServer/Controllers/WalletsController.PullPayments.cs b/BTCPayServer/Controllers/WalletsController.PullPayments.cs index 8bc9c273d..5e87fe25a 100644 --- a/BTCPayServer/Controllers/WalletsController.PullPayments.cs +++ b/BTCPayServer/Controllers/WalletsController.PullPayments.cs @@ -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); } diff --git a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs index 7a3208cb7..df0a6561f 100644 --- a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs +++ b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs @@ -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(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() == ManualPayoutProof.Type) + { + return raw.ToObject(); + } + var res = raw.ToObject( + JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode))); var network = _btcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + if (res == null) return null; res.LinkTemplate = network.BlockExplorerLink; return res; } diff --git a/BTCPayServer/Data/Payouts/BitcoinLike/PayoutTransactionOnChainBlob.cs b/BTCPayServer/Data/Payouts/BitcoinLike/PayoutTransactionOnChainBlob.cs index a9cc4c9d2..ebfd3dd24 100644 --- a/BTCPayServer/Data/Payouts/BitcoinLike/PayoutTransactionOnChainBlob.cs +++ b/BTCPayServer/Data/Payouts/BitcoinLike/PayoutTransactionOnChainBlob.cs @@ -13,6 +13,8 @@ namespace BTCPayServer.Data public HashSet Candidates { get; set; } = new HashSet(); [JsonIgnore] public string LinkTemplate { get; set; } + public string ProofType { get; } = "PayoutTransactionOnChainBlob"; + [JsonIgnore] public string Link { diff --git a/BTCPayServer/Data/Payouts/IClaimDestination.cs b/BTCPayServer/Data/Payouts/IClaimDestination.cs index f08998859..756f4faac 100644 --- a/BTCPayServer/Data/Payouts/IClaimDestination.cs +++ b/BTCPayServer/Data/Payouts/IClaimDestination.cs @@ -1,12 +1,8 @@ +using System; + namespace BTCPayServer.Data { public interface IClaimDestination { } - - public interface IPayoutProof - { - string Link { get; } - string Id { get; } - } } diff --git a/BTCPayServer/Data/Payouts/IPayoutProof.cs b/BTCPayServer/Data/Payouts/IPayoutProof.cs new file mode 100644 index 000000000..d77a6658b --- /dev/null +++ b/BTCPayServer/Data/Payouts/IPayoutProof.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Data +{ + public interface IPayoutProof + { + string ProofType { get; } + string Link { get; } + string Id { get; } + } +} diff --git a/BTCPayServer/Data/Payouts/ManualPayoutProof.cs b/BTCPayServer/Data/Payouts/ManualPayoutProof.cs new file mode 100644 index 000000000..7ac8bf162 --- /dev/null +++ b/BTCPayServer/Data/Payouts/ManualPayoutProof.cs @@ -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; } + } +} diff --git a/BTCPayServer/Data/Payouts/PayoutExtensions.cs b/BTCPayServer/Data/Payouts/PayoutExtensions.cs index b1ed0bcb3..fe9ae4549 100644 --- a/BTCPayServer/Data/Payouts/PayoutExtensions.cs +++ b/BTCPayServer/Data/Payouts/PayoutExtensions.cs @@ -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; + } + } } } diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index a52dd1d2c..0f6ab6496 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -123,8 +123,8 @@ namespace BTCPayServer.HostedServices public async Task 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 MarkPaid(PayoutPaidRequest request) + { + CancellationToken.ThrowIfCancellationRequested(); + var cts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (!_Channel.Writer.TryWrite(new InternalPayoutPaidRequest(cts, request))) + throw new ObjectDisposedException(nameof(PullPaymentHostedService)); + return cts.Task; + } + + + class InternalPayoutPaidRequest + { + public InternalPayoutPaidRequest(TaskCompletionSource 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 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 diff --git a/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs b/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs index b594dab39..e2e26e663 100644 --- a/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs +++ b/BTCPayServer/Models/WalletViewModels/PayoutsModel.cs @@ -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 diff --git a/BTCPayServer/Views/PullPayment/ViewPullPayment.cshtml b/BTCPayServer/Views/PullPayment/ViewPullPayment.cshtml index 4c6191c53..0ca5676c9 100644 --- a/BTCPayServer/Views/PullPayment/ViewPullPayment.cshtml +++ b/BTCPayServer/Views/PullPayment/ViewPullPayment.cshtml @@ -139,7 +139,7 @@
-

Awaiting Claims

+

Claims

@if (Model.Payouts.Any()) { diff --git a/BTCPayServer/Views/Wallets/Payouts.cshtml b/BTCPayServer/Views/Wallets/Payouts.cshtml index 9431492f5..324a0fd06 100644 --- a/BTCPayServer/Views/Wallets/Payouts.cshtml +++ b/BTCPayServer/Views/Wallets/Payouts.cshtml @@ -4,7 +4,7 @@ @inject IEnumerable 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; }
@@ -122,9 +123,9 @@ @if (state.State != PayoutState.AwaitingApproval) { - @if (!(pp.TransactionLink is null)) + @if (!(pp.ProofLink is null)) { - Link + Link } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json index 8b8cf6734..d68d04346 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.pull-payments.json @@ -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": {