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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 76 additions and 22 deletions

View File

@ -4,5 +4,5 @@ namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{
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))]
public DateTimeOffset? StartsAt { 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 bool Archived { 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();
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]

View File

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

View File

@ -161,7 +161,7 @@ namespace BTCPayServer.Controllers
{
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
});
}

View File

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

View File

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

View File

@ -35,6 +35,7 @@ namespace BTCPayServer.HostedServices
public string EmbeddedCSS { get; set; }
public PaymentMethodId[] PaymentMethodIds { get; set; }
public TimeSpan? Period { get; set; }
public bool AutoApproveClaims { get; set; }
public TimeSpan? BOLT11Expiration { get; set; }
}
@ -117,6 +118,7 @@ namespace BTCPayServer.HostedServices
Limit = create.Amount,
Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null,
SupportedPaymentMethods = create.PaymentMethodIds,
AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView()
{
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 =
_payoutHandlers.FindPayoutHandler(req.ClaimRequest.PaymentMethodId);
if (payoutHandler is null)
@ -484,8 +478,7 @@ namespace BTCPayServer.HostedServices
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
Date = now,
State =
req.ClaimRequest.PreApprove ? PayoutState.AwaitingPayment : PayoutState.AwaitingApproval,
State = PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id,
@ -494,7 +487,6 @@ namespace BTCPayServer.HostedServices
var payoutBlob = new PayoutBlob()
{
Amount = claimed,
CryptoAmount = req.ClaimRequest.PreApprove ? claimed : null,
Destination = req.ClaimRequest.Destination.ToString()
};
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
@ -503,6 +495,24 @@ namespace BTCPayServer.HostedServices
{
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
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));
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
new PayoutNotification()
@ -702,6 +712,6 @@ namespace BTCPayServer.HostedServices
public decimal? Value { get; set; }
public IClaimDestination Destination { 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 DateTimeOffset StartDate { get; set; }
public DateTimeOffset? EndDate { get; set; }
public bool AutoApproveClaims { get; set; }
public bool Archived { get; set; } = false;
}
@ -62,5 +63,7 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")]
[Range(1, 365 * 10)]
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.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.NotificationViewModels;
using Microsoft.AspNetCore.Routing;
@ -37,7 +38,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch
{
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()
};
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
@ -50,6 +51,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
public string StoreId { get; set; }
public string PaymentMethod { get; set; }
public string Currency { get; set; }
public PayoutState? State { get; set; }
public override string Identifier => TYPE;
public override string NotificationType => TYPE;
public PayoutState? Status { get; set; }

View File

@ -6,11 +6,11 @@
}
@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 {
<partial name="_ValidationScriptsPartial" />
<partial name="_ValidationScriptsPartial"/>
<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"/>
</div>
<partial name="_StatusMessage" />
<partial name="_StatusMessage"/>
<div class="row">
<div class="col-md-6">
<div class="form-group">
@ -33,7 +33,7 @@
<div class="row">
<div class="form-group col-8">
<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>
</div>
<div class="form-group col-4">
@ -41,6 +41,14 @@
<input asp-for="Currency" currency-selection class="form-control"/>
<span asp-validation-for="Currency" class="text-danger"></span>
</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 class="form-group mb-4">
<label asp-for="PaymentMethods" class="form-label"></label>

View File

@ -103,6 +103,7 @@
</a>
</th>
<th scope="col">Name</th>
<th scope="col">Automatically Approved</th>
<th scope="col">Refunded</th>
<th scope="col" class="text-end" >Actions</th>
</tr>
@ -113,6 +114,7 @@
<tr>
<td>@pp.StartDate.ToBrowserDate()</td>
<td>@pp.Name</td>
<td>@pp.AutoApproveClaims</td>
<td class="align-middle">
<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"