mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
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:
@@ -53,6 +53,16 @@ namespace BTCPayServer.Client
|
|||||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
||||||
return await HandleResponse<PayoutData>(response);
|
return await HandleResponse<PayoutData>(response);
|
||||||
}
|
}
|
||||||
|
public virtual async Task<PayoutData> GetPullPaymentPayout(string pullPaymentId, string payoutId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
|
||||||
|
return await HandleResponse<PayoutData>(response);
|
||||||
|
}
|
||||||
|
public virtual async Task<PayoutData> GetStorePayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts/{payoutId}", method: HttpMethod.Get), cancellationToken);
|
||||||
|
return await HandleResponse<PayoutData>(response);
|
||||||
|
}
|
||||||
public virtual async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest, CancellationToken cancellationToken = default)
|
public virtual async Task<PayoutData> CreatePayout(string storeId, CreatePayoutThroughStoreRequest payoutRequest, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
|
||||||
@@ -69,7 +79,7 @@ namespace BTCPayServer.Client
|
|||||||
return await HandleResponse<PayoutData>(response);
|
return await HandleResponse<PayoutData>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkPayoutPaid(string storeId, string payoutId,
|
public virtual async Task MarkPayoutPaid(string storeId, string payoutId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var response = await _httpClient.SendAsync(
|
var response = await _httpClient.SendAsync(
|
||||||
@@ -78,5 +88,14 @@ namespace BTCPayServer.Client
|
|||||||
method: HttpMethod.Post), cancellationToken);
|
method: HttpMethod.Post), cancellationToken);
|
||||||
await HandleResponse(response);
|
await HandleResponse(response);
|
||||||
}
|
}
|
||||||
|
public virtual async Task MarkPayout(string storeId, string payoutId, MarkPayoutRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest(
|
||||||
|
$"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}/mark",
|
||||||
|
method: HttpMethod.Post, bodyPayload: request), cancellationToken);
|
||||||
|
await HandleResponse(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
BTCPayServer.Client/Models/MarkPayoutRequest.cs
Normal file
13
BTCPayServer.Client/Models/MarkPayoutRequest.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class MarkPayoutRequest
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public PayoutState State { get; set; } = PayoutState.Completed;
|
||||||
|
|
||||||
|
public JObject? PaymentProof { get; set; }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using BTCPayServer.JsonConverters;
|
using BTCPayServer.JsonConverters;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
@@ -29,5 +30,6 @@ namespace BTCPayServer.Client.Models
|
|||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public PayoutState State { get; set; }
|
public PayoutState State { get; set; }
|
||||||
public int Revision { get; set; }
|
public int Revision { get; set; }
|
||||||
|
public JObject PaymentProof { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -797,6 +797,100 @@ namespace BTCPayServer.Tests
|
|||||||
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
|
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanProcessPayoutsExternally()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var acc = tester.NewAccount();
|
||||||
|
acc.Register();
|
||||||
|
await acc.CreateStoreAsync();
|
||||||
|
var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId;
|
||||||
|
var client = await acc.CreateClient();
|
||||||
|
var address = await tester.ExplorerNode.GetNewAddressAsync();
|
||||||
|
var payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Approved = false,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Amount = 0.0001m,
|
||||||
|
Destination = address.ToString()
|
||||||
|
});
|
||||||
|
await AssertAPIError("invalid-state", async () =>
|
||||||
|
{
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||||
|
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed});
|
||||||
|
Assert.Equal(PayoutState.Completed,(await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).State );
|
||||||
|
Assert.Null((await client.GetStorePayouts(storeId,false)).Single(data => data.Id == payout.Id ).PaymentProof );
|
||||||
|
|
||||||
|
foreach (var state in new []{ PayoutState.AwaitingApproval, PayoutState.Cancelled, PayoutState.Completed, PayoutState.AwaitingApproval, PayoutState.InProgress})
|
||||||
|
{
|
||||||
|
await AssertAPIError("invalid-state", async () =>
|
||||||
|
{
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = state});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payout = await client.CreatePayout(storeId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Amount = 0.0001m,
|
||||||
|
Destination = address.ToString()
|
||||||
|
});
|
||||||
|
|
||||||
|
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||||
|
Assert.NotNull(payout);
|
||||||
|
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||||
|
await AssertValidationError(new []{"PaymentProof"}, async () =>
|
||||||
|
{
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
|
||||||
|
{
|
||||||
|
test = "zyx"
|
||||||
|
})});
|
||||||
|
});
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.InProgress, PaymentProof = JObject.FromObject(new
|
||||||
|
{
|
||||||
|
proofType = "external-proof"
|
||||||
|
})});
|
||||||
|
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||||
|
Assert.NotNull(payout);
|
||||||
|
Assert.Equal(PayoutState.InProgress, payout.State);
|
||||||
|
Assert.True(payout.PaymentProof.TryGetValue("proofType", out var savedType));
|
||||||
|
Assert.Equal("external-proof",savedType);
|
||||||
|
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.AwaitingPayment, PaymentProof = JObject.FromObject(new
|
||||||
|
{
|
||||||
|
proofType = "external-proof",
|
||||||
|
id="finality proof",
|
||||||
|
link="proof.com"
|
||||||
|
})});
|
||||||
|
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||||
|
Assert.NotNull(payout);
|
||||||
|
Assert.Null(payout.PaymentProof);
|
||||||
|
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||||
|
|
||||||
|
await client.MarkPayout(storeId, payout.Id, new MarkPayoutRequest() {State = PayoutState.Completed, PaymentProof = JObject.FromObject(new
|
||||||
|
{
|
||||||
|
proofType = "external-proof",
|
||||||
|
id="finality proof",
|
||||||
|
link="proof.com"
|
||||||
|
})});
|
||||||
|
payout = await client.GetStorePayout(storeId, payout.Id);
|
||||||
|
Assert.NotNull(payout);
|
||||||
|
Assert.Equal(PayoutState.Completed, payout.State);
|
||||||
|
Assert.True(payout.PaymentProof.TryGetValue("proofType", out savedType));
|
||||||
|
Assert.True(payout.PaymentProof.TryGetValue("link", out var savedLink));
|
||||||
|
Assert.True(payout.PaymentProof.TryGetValue("id", out var savedId));
|
||||||
|
Assert.Equal("external-proof",savedType);
|
||||||
|
Assert.Equal("finality proof",savedId);
|
||||||
|
Assert.Equal("proof.com",savedLink);
|
||||||
|
}
|
||||||
|
|
||||||
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
|
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
|
||||||
{
|
{
|
||||||
return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
|
return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Cors;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers.Greenfield
|
namespace BTCPayServer.Controllers.Greenfield
|
||||||
{
|
{
|
||||||
@@ -243,6 +244,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
model.Destination = blob.Destination;
|
model.Destination = blob.Destination;
|
||||||
model.PaymentMethod = p.PaymentMethodId;
|
model.PaymentMethod = p.PaymentMethodId;
|
||||||
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
|
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
|
||||||
|
model.PaymentProof = p.GetProofBlobJson();
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,19 +419,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
|
|
||||||
|
|
||||||
return base.Ok(payouts
|
return base.Ok(payouts
|
||||||
.Select(ToModel).ToList());
|
.Select(ToModel).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
[HttpDelete("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
||||||
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> CancelPayout(string storeId, string payoutId)
|
public async Task<IActionResult> CancelPayout(string storeId, string payoutId)
|
||||||
{
|
{
|
||||||
using var ctx = _dbContextFactory.CreateContext();
|
var res= await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }, new []{storeId}));
|
||||||
var payout = await ctx.Payouts.GetPayout(payoutId, storeId);
|
return MapResult(res.First().Value);
|
||||||
if (payout is null)
|
|
||||||
return PayoutNotFound();
|
|
||||||
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(new[] { payoutId }));
|
|
||||||
return Ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
[HttpPost("~/api/v1/stores/{storeId}/payouts/{payoutId}")]
|
||||||
@@ -490,29 +488,67 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> MarkPayoutPaid(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
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)
|
if (!ModelState.IsValid)
|
||||||
return this.CreateValidationError(ModelState);
|
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 = request.PaymentProof,
|
||||||
Proof = null,
|
PayoutId = payoutId,
|
||||||
PayoutId = payoutId
|
State = request.State
|
||||||
});
|
});
|
||||||
var errorMessage = PayoutPaidRequest.GetErrorMessage(result);
|
return MapResult(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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()
|
private IActionResult PayoutNotFound()
|
||||||
{
|
{
|
||||||
return this.CreateAPIError(404, "payout-not-found", "The payout was not found");
|
return this.CreateAPIError(404, "payout-not-found", "The payout was not found");
|
||||||
|
|||||||
@@ -1168,5 +1168,26 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
|
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task MarkPayoutPaid(string storeId, string payoutId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
HandleActionResult(await GetController<GreenfieldPullPaymentController>().MarkPayoutPaid(storeId, payoutId, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task MarkPayout(string storeId, string payoutId, MarkPayoutRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
HandleActionResult(await GetController<GreenfieldPullPaymentController>().MarkPayout(storeId, payoutId, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PayoutData> GetPullPaymentPayout(string pullPaymentId, string payoutId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<PayoutData>(await GetController<GreenfieldPullPaymentController>().GetPayout(pullPaymentId, payoutId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PayoutData> GetStorePayout(string storeId, string payoutId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<PayoutData>(await GetController<GreenfieldPullPaymentController>().GetStorePayout(storeId, payoutId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ using Microsoft.AspNetCore.Routing;
|
|||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Crypto;
|
using NBitcoin.Crypto;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
@@ -167,9 +168,9 @@ namespace BTCPayServer
|
|||||||
switch (payResult.Result)
|
switch (payResult.Result)
|
||||||
{
|
{
|
||||||
case PayResult.Ok:
|
case PayResult.Ok:
|
||||||
await _pullPaymentHostedService.MarkPaid(new PayoutPaidRequest()
|
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
|
||||||
{
|
{
|
||||||
PayoutId = claimResponse.PayoutData.Id, Proof = new ManualPayoutProof { }
|
PayoutId = claimResponse.PayoutData.Id, State = PayoutState.Completed
|
||||||
});
|
});
|
||||||
|
|
||||||
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
return Ok(new LNUrlStatusResponse {Status = "OK"});
|
||||||
@@ -178,7 +179,7 @@ namespace BTCPayServer
|
|||||||
new PullPaymentHostedService.CancelRequest(new string[]
|
new PullPaymentHostedService.CancelRequest(new string[]
|
||||||
{
|
{
|
||||||
claimResponse.PayoutData.Id
|
claimResponse.PayoutData.Id
|
||||||
}));
|
}, null));
|
||||||
|
|
||||||
return Ok(new LNUrlStatusResponse
|
return Ok(new LNUrlStatusResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
@@ -391,12 +392,12 @@ namespace BTCPayServer.Controllers
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var result =
|
var result =
|
||||||
await _pullPaymentService.MarkPaid(new PayoutPaidRequest() { PayoutId = payout.Id });
|
await _pullPaymentService.MarkPaid(new MarkPayoutRequest() { PayoutId = payout.Id });
|
||||||
if (result != PayoutPaidRequest.PayoutPaidResult.Ok)
|
if (result != MarkPayoutRequest.PayoutPaidResult.Ok)
|
||||||
{
|
{
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = PayoutPaidRequest.GetErrorMessage(result),
|
Message = MarkPayoutRequest.GetErrorMessage(result),
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error
|
Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
});
|
});
|
||||||
return RedirectToAction(nameof(Payouts),
|
return RedirectToAction(nameof(Payouts),
|
||||||
@@ -418,7 +419,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
case "cancel":
|
case "cancel":
|
||||||
await _pullPaymentService.Cancel(
|
await _pullPaymentService.Cancel(
|
||||||
new PullPaymentHostedService.CancelRequest(payoutIds));
|
new PullPaymentHostedService.CancelRequest(payoutIds, new[] {storeId}));
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
{
|
{
|
||||||
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
|
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
|
|||||||
@@ -107,10 +107,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
ParseProofType(payout.Proof, out var raw, out var proofType);
|
ParseProofType(payout.Proof, out var raw, out var proofType);
|
||||||
if (proofType == ManualPayoutProof.Type)
|
if (proofType == PayoutTransactionOnChainBlob.Type)
|
||||||
{
|
{
|
||||||
return raw.ToObject<ManualPayoutProof>();
|
|
||||||
}
|
|
||||||
var res = raw.ToObject<PayoutTransactionOnChainBlob>(
|
var res = raw.ToObject<PayoutTransactionOnChainBlob>(
|
||||||
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode)));
|
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode)));
|
||||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
@@ -119,6 +118,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
res.LinkTemplate = network.BlockExplorerLink;
|
res.LinkTemplate = network.BlockExplorerLink;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
return raw.ToObject<ManualPayoutProof>();
|
||||||
|
}
|
||||||
|
|
||||||
public static void ParseProofType(byte[] proof, out JObject obj, out string type)
|
public static void ParseProofType(byte[] proof, out JObject obj, out string type)
|
||||||
{
|
{
|
||||||
@@ -130,10 +131,21 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj = JObject.Parse(Encoding.UTF8.GetString(proof));
|
obj = JObject.Parse(Encoding.UTF8.GetString(proof));
|
||||||
if (obj.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType))
|
TryParseProofType(obj, out type);
|
||||||
{
|
|
||||||
type = proofType.Value<string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseProofType(JObject proof, out string type)
|
||||||
|
{
|
||||||
|
type = null;
|
||||||
|
if (proof is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proof.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType))
|
||||||
|
return false;
|
||||||
|
type = proofType.Value<string>();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartBackgroundCheck(Action<Type[]> subscribe)
|
public void StartBackgroundCheck(Action<Type[]> subscribe)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace BTCPayServer.Data
|
|||||||
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
|
public HashSet<uint256> Candidates { get; set; } = new HashSet<uint256>();
|
||||||
|
|
||||||
[JsonIgnore] public string LinkTemplate { get; set; }
|
[JsonIgnore] public string LinkTemplate { get; set; }
|
||||||
public string ProofType { get; } = "PayoutTransactionOnChainBlob";
|
public string ProofType { get; } = Type;
|
||||||
|
public const string Type = "PayoutTransactionOnChainBlob";
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string Link
|
public string Link
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (proofType == ManualPayoutProof.Type)
|
if (proofType == PayoutLightningBlob.PayoutLightningBlobProofType)
|
||||||
{
|
{
|
||||||
return raw.ToObject<ManualPayoutProof>();
|
return raw.ToObject<PayoutLightningBlob>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.ToObject<PayoutLightningBlob>();
|
return raw.ToObject<ManualPayoutProof>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartBackgroundCheck(Action<Type[]> subscribe)
|
public void StartBackgroundCheck(Action<Type[]> subscribe)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
{
|
{
|
||||||
public string PaymentHash { get; set; }
|
public string PaymentHash { get; set; }
|
||||||
|
|
||||||
public string ProofType { get; } = "PayoutLightningBlob";
|
public static string PayoutLightningBlobProofType = "PayoutLightningBlob";
|
||||||
|
public string ProofType { get; } = PayoutLightningBlobProofType;
|
||||||
public string Link { get; } = null;
|
public string Link { get; } = null;
|
||||||
public string Id => PaymentHash;
|
public string Id => PaymentHash;
|
||||||
public string Preimage { get; set; }
|
public string Preimage { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public class ManualPayoutProof : IPayoutProof
|
public class ManualPayoutProof : IPayoutProof
|
||||||
@@ -6,5 +10,7 @@ namespace BTCPayServer.Data
|
|||||||
public string ProofType { get; } = Type;
|
public string ProofType { get; } = Type;
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,7 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null;
|
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
return JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
||||||
@@ -40,12 +42,30 @@ namespace BTCPayServer.Data
|
|||||||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
|
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JObject? GetProofBlobJson(this PayoutData data)
|
||||||
|
{
|
||||||
|
return data?.Proof is null ? null : JObject.Parse(Encoding.UTF8.GetString(data.Proof));
|
||||||
|
}
|
||||||
public static void SetProofBlob(this PayoutData data, IPayoutProof blob, JsonSerializerSettings settings)
|
public static void SetProofBlob(this PayoutData data, IPayoutProof blob, JsonSerializerSettings settings)
|
||||||
{
|
{
|
||||||
if (blob is null)
|
if (blob is null)
|
||||||
|
{
|
||||||
|
data.Proof = null;
|
||||||
return;
|
return;
|
||||||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, settings));
|
}
|
||||||
|
|
||||||
|
data.SetProofBlob(settings is null
|
||||||
|
? JObject.FromObject(blob)
|
||||||
|
: JObject.FromObject(blob, JsonSerializer.Create(settings)));
|
||||||
|
}
|
||||||
|
public static void SetProofBlob(this PayoutData data, JObject blob)
|
||||||
|
{
|
||||||
|
if (blob is null)
|
||||||
|
{
|
||||||
|
data.Proof = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(blob.ToString(Formatting.None));
|
||||||
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
|
// 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))
|
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||||
|
|
||||||
@@ -53,15 +54,18 @@ namespace BTCPayServer.HostedServices
|
|||||||
PullPaymentId = pullPaymentId;
|
PullPaymentId = pullPaymentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CancelRequest(string[] payoutIds)
|
public CancelRequest(string[] payoutIds, string[] storeIds)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(payoutIds);
|
ArgumentNullException.ThrowIfNull(payoutIds);
|
||||||
PayoutIds = payoutIds;
|
PayoutIds = payoutIds;
|
||||||
|
StoreIds = storeIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] StoreIds { get; set; }
|
||||||
|
|
||||||
public string PullPaymentId { get; set; }
|
public string PullPaymentId { get; set; }
|
||||||
public string[] PayoutIds { get; set; }
|
public string[] PayoutIds { get; set; }
|
||||||
internal TaskCompletionSource<bool> Completion { get; set; }
|
internal TaskCompletionSource<Dictionary<string, MarkPayoutRequest.PayoutPaidResult>> Completion { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PayoutApproval
|
public class PayoutApproval
|
||||||
@@ -410,24 +414,32 @@ namespace BTCPayServer.HostedServices
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (payout is null)
|
if (payout is null)
|
||||||
{
|
{
|
||||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.NotFound);
|
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.NotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payout.State != PayoutState.AwaitingPayment)
|
if (payout.State == PayoutState.Completed)
|
||||||
{
|
{
|
||||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.InvalidState);
|
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.InvalidState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
switch (req.Request.State)
|
||||||
if (req.Request.Proof != null)
|
|
||||||
{
|
{
|
||||||
payout.SetProofBlob(req.Request.Proof, null);
|
case PayoutState.Completed or PayoutState.InProgress
|
||||||
|
when payout.State is not PayoutState.AwaitingPayment and not PayoutState.Completed and not PayoutState.InProgress :
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
payout.State = req.Request.State;
|
||||||
payout.State = PayoutState.Completed;
|
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
req.Completion.SetResult(PayoutPaidRequest.PayoutPaidResult.Ok);
|
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -604,24 +616,42 @@ namespace BTCPayServer.HostedServices
|
|||||||
.Property(o => o.Archived).IsModified = true;
|
.Property(o => o.Archived).IsModified = true;
|
||||||
payouts = await ctx.Payouts
|
payouts = await ctx.Payouts
|
||||||
.Where(p => p.PullPaymentDataId == cancel.PullPaymentId)
|
.Where(p => p.PullPaymentDataId == cancel.PullPaymentId)
|
||||||
|
.Where(p => cancel.StoreIds == null || cancel.StoreIds.Contains(p.StoreDataId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
cancel.PayoutIds = payouts.Select(data => data.Id).ToArray();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var payoutIds = cancel.PayoutIds.ToHashSet();
|
var payoutIds = cancel.PayoutIds.ToHashSet();
|
||||||
payouts = await ctx.Payouts
|
payouts = await ctx.Payouts
|
||||||
.Where(p => payoutIds.Contains(p.Id))
|
.Where(p => payoutIds.Contains(p.Id))
|
||||||
|
.Where(p => cancel.StoreIds == null || cancel.StoreIds.Contains(p.StoreDataId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dictionary<string, MarkPayoutRequest.PayoutPaidResult> result = new();
|
||||||
|
|
||||||
foreach (var payout in payouts)
|
foreach (var payout in payouts)
|
||||||
{
|
{
|
||||||
if (payout.State != PayoutState.Completed && payout.State != PayoutState.InProgress)
|
if (payout.State != PayoutState.Completed && payout.State != PayoutState.InProgress)
|
||||||
|
{
|
||||||
payout.State = PayoutState.Cancelled;
|
payout.State = PayoutState.Cancelled;
|
||||||
|
result.Add(payout.Id, MarkPayoutRequest.PayoutPaidResult.Ok);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(payout.Id, MarkPayoutRequest.PayoutPaidResult.InvalidState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string s1 in cancel.PayoutIds.Where(s => !result.ContainsKey(s)))
|
||||||
|
{
|
||||||
|
result.Add(s1, MarkPayoutRequest.PayoutPaidResult.NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
cancel.Completion.TrySetResult(true);
|
cancel.Completion.TrySetResult(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -629,14 +659,13 @@ namespace BTCPayServer.HostedServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Cancel(CancelRequest cancelRequest)
|
public Task<Dictionary<string, MarkPayoutRequest.PayoutPaidResult>> Cancel(CancelRequest cancelRequest)
|
||||||
{
|
{
|
||||||
CancellationToken.ThrowIfCancellationRequested();
|
CancellationToken.ThrowIfCancellationRequested();
|
||||||
var cts = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
cancelRequest.Completion = new TaskCompletionSource<Dictionary<string, MarkPayoutRequest.PayoutPaidResult>>();
|
||||||
cancelRequest.Completion = cts;
|
|
||||||
if (!_Channel.Writer.TryWrite(cancelRequest))
|
if (!_Channel.Writer.TryWrite(cancelRequest))
|
||||||
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
||||||
return cts.Task;
|
return cancelRequest.Completion.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ClaimRequest.ClaimResponse> Claim(ClaimRequest request)
|
public Task<ClaimRequest.ClaimResponse> Claim(ClaimRequest request)
|
||||||
@@ -656,10 +685,10 @@ namespace BTCPayServer.HostedServices
|
|||||||
return base.StopAsync(cancellationToken);
|
return base.StopAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PayoutPaidRequest.PayoutPaidResult> MarkPaid(PayoutPaidRequest request)
|
public Task<MarkPayoutRequest.PayoutPaidResult> MarkPaid(MarkPayoutRequest request)
|
||||||
{
|
{
|
||||||
CancellationToken.ThrowIfCancellationRequested();
|
CancellationToken.ThrowIfCancellationRequested();
|
||||||
var cts = new TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult>(TaskCreationOptions
|
var cts = new TaskCompletionSource<MarkPayoutRequest.PayoutPaidResult>(TaskCreationOptions
|
||||||
.RunContinuationsAsynchronously);
|
.RunContinuationsAsynchronously);
|
||||||
if (!_Channel.Writer.TryWrite(new InternalPayoutPaidRequest(cts, request)))
|
if (!_Channel.Writer.TryWrite(new InternalPayoutPaidRequest(cts, request)))
|
||||||
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
throw new ObjectDisposedException(nameof(PullPaymentHostedService));
|
||||||
@@ -710,8 +739,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
|
|
||||||
class InternalPayoutPaidRequest
|
class InternalPayoutPaidRequest
|
||||||
{
|
{
|
||||||
public InternalPayoutPaidRequest(TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> completionSource,
|
public InternalPayoutPaidRequest(TaskCompletionSource<MarkPayoutRequest.PayoutPaidResult> completionSource,
|
||||||
PayoutPaidRequest request)
|
MarkPayoutRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
ArgumentNullException.ThrowIfNull(completionSource);
|
ArgumentNullException.ThrowIfNull(completionSource);
|
||||||
@@ -719,12 +748,12 @@ namespace BTCPayServer.HostedServices
|
|||||||
Request = request;
|
Request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TaskCompletionSource<PayoutPaidRequest.PayoutPaidResult> Completion { get; set; }
|
public TaskCompletionSource<MarkPayoutRequest.PayoutPaidResult> Completion { get; set; }
|
||||||
public PayoutPaidRequest Request { get; }
|
public MarkPayoutRequest Request { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PayoutPaidRequest
|
public class MarkPayoutRequest
|
||||||
{
|
{
|
||||||
public enum PayoutPaidResult
|
public enum PayoutPaidResult
|
||||||
{
|
{
|
||||||
@@ -734,7 +763,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string PayoutId { get; set; }
|
public string PayoutId { get; set; }
|
||||||
public ManualPayoutProof Proof { get; set; }
|
public JObject? Proof { get; set; }
|
||||||
|
public PayoutState State { get; set; }
|
||||||
|
|
||||||
public static string GetErrorMessage(PayoutPaidResult result)
|
public static string GetErrorMessage(PayoutPaidResult result)
|
||||||
{
|
{
|
||||||
@@ -745,7 +775,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
case PayoutPaidResult.Ok:
|
case PayoutPaidResult.Ok:
|
||||||
return "Ok";
|
return "Ok";
|
||||||
case PayoutPaidResult.InvalidState:
|
case PayoutPaidResult.InvalidState:
|
||||||
return "The payout is not in a state that can be marked as paid";
|
return "The payout is not in a state that can be marked with the specified state";
|
||||||
default:
|
default:
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -534,6 +534,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"get": {
|
||||||
|
"summary": "Get Payout",
|
||||||
|
"operationId": "GetStorePayout",
|
||||||
|
"description": "Get payout",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A specific payout of a store",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PayoutData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Payout not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Stores (Payouts)"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmanagepullpayments"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Approve Payout",
|
"summary": "Approve Payout",
|
||||||
"operationId": "PullPayments_ApprovePayout",
|
"operationId": "PullPayments_ApprovePayout",
|
||||||
@@ -696,6 +727,89 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/payouts/{payoutId}/mark": {
|
||||||
|
"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",
|
||||||
|
"operationId": "PullPayments_MarkPayout",
|
||||||
|
"description": "Mark a payout with a state",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"$ref": "#/components/schemas/PayoutState"
|
||||||
|
},
|
||||||
|
"paymentProof": {
|
||||||
|
"$ref": "#/components/schemas/PayoutPaymentProof"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The payout has been set to the specified 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": [
|
||||||
|
"Stores (Payouts)"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmanagepullpayments"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -753,6 +867,57 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"PayoutPaymentProof": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"description": "Additional information around how the payout is being or has been paid out. The mentioned properties are all optional (except `proofType`) and you can introduce any json format you wish.",
|
||||||
|
"properties": {
|
||||||
|
"proofType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The type of payment proof it is."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"link": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "url",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A link to the proof of payout payment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier to the proof of payout payment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PayoutState": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "AwaitingPayment",
|
||||||
|
"description": "The state of the payout (`AwaitingApproval`, `AwaitingPayment`, `InProgress`, `Completed`, `Cancelled`)",
|
||||||
|
"x-enumNames": [
|
||||||
|
"AwaitingApproval",
|
||||||
|
"AwaitingPayment",
|
||||||
|
"InProgress",
|
||||||
|
"Completed",
|
||||||
|
"Cancelled"
|
||||||
|
],
|
||||||
|
"enum": [
|
||||||
|
"AwaitingApproval",
|
||||||
|
"AwaitingPayment",
|
||||||
|
"InProgress",
|
||||||
|
"Completed",
|
||||||
|
"Cancelled"
|
||||||
|
]
|
||||||
|
},
|
||||||
"PayoutData": {
|
"PayoutData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -801,23 +966,10 @@
|
|||||||
"description": "The amount of the payout in the currency of the payment method (eg. BTC). This is only available from the `AwaitingPayment` state."
|
"description": "The amount of the payout in the currency of the payment method (eg. BTC). This is only available from the `AwaitingPayment` state."
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"type": "string",
|
"$ref": "#/components/schemas/PayoutState"
|
||||||
"example": "AwaitingPayment",
|
},
|
||||||
"description": "The state of the payout (`AwaitingApproval`, `AwaitingPayment`, `InProgress`, `Completed`, `Cancelled`)",
|
"paymentProof": {
|
||||||
"x-enumNames": [
|
"$ref": "#/components/schemas/PayoutPaymentProof"
|
||||||
"AwaitingApproval",
|
|
||||||
"AwaitingPayment",
|
|
||||||
"InProgress",
|
|
||||||
"Completed",
|
|
||||||
"Cancelled"
|
|
||||||
],
|
|
||||||
"enum": [
|
|
||||||
"AwaitingApproval",
|
|
||||||
"AwaitingPayment",
|
|
||||||
"InProgress",
|
|
||||||
"Completed",
|
|
||||||
"Cancelled"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user