Allow auto approval of claims for pull payments (#1851)

* Allow auto approval of claims for pull payments

closes #1780

* fix
This commit is contained in:
Andrew Camilleri
2022-04-28 02:51:04 +02:00
committed by GitHub
parent 273bc78db3
commit ed1a7bb887
13 changed files with 76 additions and 22 deletions

View File

@@ -4,5 +4,5 @@ namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{ {
public string? PullPaymentId { get; set; } public string? PullPaymentId { get; set; }
public bool Approved { get; set; } public bool? Approved { get; set; }
} }

View File

@@ -22,5 +22,6 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? StartsAt { get; set; } public DateTimeOffset? StartsAt { get; set; }
public string[] PaymentMethods { get; set; } public string[] PaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; }
} }
} }

View File

@@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
public TimeSpan BOLT11Expiration { get; set; } public TimeSpan BOLT11Expiration { get; set; }
public bool Archived { get; set; } public bool Archived { get; set; }
public string ViewLink { get; set; } public string ViewLink { get; set; }
public bool AutoApproveClaims { get; set; }
} }
} }

View File

@@ -1412,6 +1412,28 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click(); s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource); Assert.Contains(bolt, s.Driver.PageSource);
} }
//auto-approve pull payments
s.GoToStore(StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.FindElement(By.LinkText("View")).Click();
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
} }
[Fact] [Fact]

View File

@@ -135,7 +135,8 @@ namespace BTCPayServer.Controllers.Greenfield
Amount = request.Amount, Amount = request.Amount,
Currency = request.Currency, Currency = request.Currency,
StoreId = storeId, StoreId = storeId,
PaymentMethodIds = paymentMethods PaymentMethodIds = paymentMethods,
AutoApproveClaims = request.AutoApproveClaims
}); });
var pp = await _pullPaymentService.GetPullPayment(ppId, false); var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp)); return this.Ok(CreatePullPaymentData(pp));
@@ -155,6 +156,7 @@ namespace BTCPayServer.Controllers.Greenfield
Currency = ppBlob.Currency, Currency = ppBlob.Currency,
Period = ppBlob.Period, Period = ppBlob.Period,
Archived = pp.Archived, Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration, BOLT11Expiration = ppBlob.BOLT11Expiration,
ViewLink = _linkGenerator.GetUriByAction( ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment), nameof(UIPullPaymentController.ViewPullPayment),

View File

@@ -161,7 +161,7 @@ namespace BTCPayServer.Controllers
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting approval.", Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval? "approval": "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success Severity = StatusMessageModel.StatusSeverity.Success
}); });
} }

View File

@@ -139,7 +139,8 @@ namespace BTCPayServer.Controllers
PaymentMethodIds = selectedPaymentMethodIds, PaymentMethodIds = selectedPaymentMethodIds,
EmbeddedCSS = model.EmbeddedCSS, EmbeddedCSS = model.EmbeddedCSS,
CustomCSSLink = model.CustomCSSLink, CustomCSSLink = model.CustomCSSLink,
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration) BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
AutoApproveClaims = model.AutoApproveClaims
}); });
this.TempData.SetStatusMessageModel(new StatusMessageModel() this.TempData.SetStatusMessageModel(new StatusMessageModel()
{ {

View File

@@ -30,6 +30,8 @@ namespace BTCPayServer.Data
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))] [JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; } public PaymentMethodId[] SupportedPaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; }
public class PullPaymentView public class PullPaymentView
{ {
public string Title { get; set; } public string Title { get; set; }

View File

@@ -35,6 +35,7 @@ namespace BTCPayServer.HostedServices
public string EmbeddedCSS { get; set; } public string EmbeddedCSS { get; set; }
public PaymentMethodId[] PaymentMethodIds { get; set; } public PaymentMethodId[] PaymentMethodIds { get; set; }
public TimeSpan? Period { get; set; } public TimeSpan? Period { get; set; }
public bool AutoApproveClaims { get; set; }
public TimeSpan? BOLT11Expiration { get; set; } public TimeSpan? BOLT11Expiration { get; set; }
} }
@@ -117,6 +118,7 @@ namespace BTCPayServer.HostedServices
Limit = create.Amount, Limit = create.Amount,
Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null, Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null,
SupportedPaymentMethods = create.PaymentMethodIds, SupportedPaymentMethods = create.PaymentMethodIds,
AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView() View = new PullPaymentBlob.PullPaymentView()
{ {
Title = create.Name ?? string.Empty, Title = create.Name ?? string.Empty,
@@ -422,14 +424,6 @@ namespace BTCPayServer.HostedServices
} }
} }
if (req.ClaimRequest.PreApprove && !withoutPullPayment &&
ppBlob.Currency != req.ClaimRequest.PaymentMethodId.CryptoCode)
{
req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
return;
}
var payoutHandler = var payoutHandler =
_payoutHandlers.FindPayoutHandler(req.ClaimRequest.PaymentMethodId); _payoutHandlers.FindPayoutHandler(req.ClaimRequest.PaymentMethodId);
if (payoutHandler is null) if (payoutHandler is null)
@@ -484,8 +478,7 @@ namespace BTCPayServer.HostedServices
{ {
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)), Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
Date = now, Date = now,
State = State = PayoutState.AwaitingApproval,
req.ClaimRequest.PreApprove ? PayoutState.AwaitingPayment : PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId, PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(), PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id, Destination = req.ClaimRequest.Destination.Id,
@@ -494,7 +487,6 @@ namespace BTCPayServer.HostedServices
var payoutBlob = new PayoutBlob() var payoutBlob = new PayoutBlob()
{ {
Amount = claimed, Amount = claimed,
CryptoAmount = req.ClaimRequest.PreApprove ? claimed : null,
Destination = req.ClaimRequest.Destination.ToString() Destination = req.ClaimRequest.Destination.ToString()
}; };
payout.SetBlob(payoutBlob, _jsonSerializerSettings); payout.SetBlob(payoutBlob, _jsonSerializerSettings);
@@ -503,6 +495,24 @@ namespace BTCPayServer.HostedServices
{ {
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination); await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true) )
{
payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId);
var rateResult = await GetRate(payout, null, CancellationToken.None);
if (rateResult.BidAsk != null)
{
var approveResult = new TaskCompletionSource<PayoutApproval.Result>();
await HandleApproval(new PayoutApproval()
{
PayoutId = payout.Id, Revision = payoutBlob.Revision, Rate = rateResult.BidAsk.Ask, Completion =approveResult
});
if ((await approveResult.Task) == PayoutApproval.Result.Ok)
{
payout.State = PayoutState.AwaitingPayment;
}
}
}
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout)); req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId), await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new PayoutNotification() new PayoutNotification()
@@ -702,6 +712,6 @@ namespace BTCPayServer.HostedServices
public decimal? Value { get; set; } public decimal? Value { get; set; }
public IClaimDestination Destination { get; set; } public IClaimDestination Destination { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
public bool PreApprove { get; set; } public bool? PreApprove { get; set; }
} }
} }

View File

@@ -28,6 +28,7 @@ namespace BTCPayServer.Models.WalletViewModels
public ProgressModel Progress { get; set; } public ProgressModel Progress { get; set; }
public DateTimeOffset StartDate { get; set; } public DateTimeOffset StartDate { get; set; }
public DateTimeOffset? EndDate { get; set; } public DateTimeOffset? EndDate { get; set; }
public bool AutoApproveClaims { get; set; }
public bool Archived { get; set; } = false; public bool Archived { get; set; } = false;
} }
@@ -62,5 +63,7 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")] [Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")]
[Range(1, 365 * 10)] [Range(1, 365 * 10)]
public long BOLT11Expiration { get; set; } = 30; public long BOLT11Expiration { get; set; } = 30;
[Display(Name = "Automatically approve claims")]
public bool AutoApproveClaims { get; set; } = false;
} }
} }

View File

@@ -3,6 +3,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.NotificationViewModels; using BTCPayServer.Models.NotificationViewModels;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
@@ -37,7 +38,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch
{ {
PayoutState.AwaitingApproval => $"A new payout is awaiting for approval", PayoutState.AwaitingApproval => $"A new payout is awaiting for approval",
PayoutState.AwaitingPayment => $"A new payout is awaiting for payment", PayoutState.AwaitingPayment => $"A new payout is approved and awaiting payment",
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
}; };
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts), vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
@@ -50,6 +51,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
public string StoreId { get; set; } public string StoreId { get; set; }
public string PaymentMethod { get; set; } public string PaymentMethod { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public PayoutState? State { get; set; }
public override string Identifier => TYPE; public override string Identifier => TYPE;
public override string NotificationType => TYPE; public override string NotificationType => TYPE;
public PayoutState? Status { get; set; } public PayoutState? Status { get; set; }

View File

@@ -6,11 +6,11 @@
} }
@section PageHeadContent { @section PageHeadContent {
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" /> <link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true"/>
} }
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial"/>
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script> <script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
} }
@@ -21,8 +21,8 @@
<input type="submit" value="Create" class="btn btn-primary" id="Create"/> <input type="submit" value="Create" class="btn btn-primary" id="Create"/>
</div> </div>
<partial name="_StatusMessage" /> <partial name="_StatusMessage"/>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
@@ -33,7 +33,7 @@
<div class="row"> <div class="row">
<div class="form-group col-8"> <div class="form-group col-8">
<label asp-for="Amount" class="form-label" data-required></label> <label asp-for="Amount" class="form-label" data-required></label>
<input asp-for="Amount" class="form-control" inputmode="decimal" /> <input asp-for="Amount" class="form-control" inputmode="decimal"/>
<span asp-validation-for="Amount" class="text-danger"></span> <span asp-validation-for="Amount" class="text-danger"></span>
</div> </div>
<div class="form-group col-4"> <div class="form-group col-4">
@@ -41,6 +41,14 @@
<input asp-for="Currency" currency-selection class="form-control"/> <input asp-for="Currency" currency-selection class="form-control"/>
<span asp-validation-for="Currency" class="text-danger"></span> <span asp-validation-for="Currency" class="text-danger"></span>
</div> </div>
<div class="form-group col-12">
<div class="form-check ">
<input asp-for="AutoApproveClaims" type="checkbox" class="form-check-input"/>
<label asp-for="AutoApproveClaims" class="form-check-label"></label>
<span asp-validation-for="AutoApproveClaims" class="text-danger"></span>
</div>
</div>
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label asp-for="PaymentMethods" class="form-label"></label> <label asp-for="PaymentMethods" class="form-label"></label>

View File

@@ -103,6 +103,7 @@
</a> </a>
</th> </th>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">Automatically Approved</th>
<th scope="col">Refunded</th> <th scope="col">Refunded</th>
<th scope="col" class="text-end" >Actions</th> <th scope="col" class="text-end" >Actions</th>
</tr> </tr>
@@ -113,6 +114,7 @@
<tr> <tr>
<td>@pp.StartDate.ToBrowserDate()</td> <td>@pp.StartDate.ToBrowserDate()</td>
<td>@pp.Name</td> <td>@pp.Name</td>
<td>@pp.AutoApproveClaims</td>
<td class="align-middle"> <td class="align-middle">
<div class="progress ppProgress" data-pp="@pp.Id" data-bs-toggle="tooltip" data-bs-html="true"> <div class="progress ppProgress" data-pp="@pp.Id" data-bs-toggle="tooltip" data-bs-html="true">
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.CompletedPercent" <div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.CompletedPercent"