Prevent payout double send (#5931)

Fixes #5913.
This commit is contained in:
d11n
2024-04-15 13:25:17 +02:00
committed by GitHub
parent f1a04a3bd0
commit 7e80fd2e98
3 changed files with 28 additions and 17 deletions

View File

@@ -10,10 +10,9 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Rating; using BTCPayServer.PayoutProcessors;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -23,7 +22,6 @@ using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
using PayoutData = BTCPayServer.Data.PayoutData; using PayoutData = BTCPayServer.Data.PayoutData;
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
@@ -40,6 +38,8 @@ namespace BTCPayServer.Controllers
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly PayoutProcessorService _payoutProcessorService;
private readonly IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
public StoreData CurrentStore public StoreData CurrentStore
{ {
@@ -55,6 +55,8 @@ namespace BTCPayServer.Controllers
DisplayFormatter displayFormatter, DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
PayoutProcessorService payoutProcessorService,
IEnumerable<IPayoutProcessorFactory> payoutProcessorFactories,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
IAuthorizationService authorizationService) IAuthorizationService authorizationService)
{ {
@@ -66,8 +68,10 @@ namespace BTCPayServer.Controllers
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings; _jsonSerializerSettings = jsonSerializerSettings;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_payoutProcessorService = payoutProcessorService;
_payoutProcessorFactories = payoutProcessorFactories;
} }
[HttpGet("stores/{storeId}/pull-payments/new")] [HttpGet("stores/{storeId}/pull-payments/new")]
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId) public async Task<IActionResult> NewPullPayment(string storeId)
@@ -287,6 +291,7 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData()); vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId);
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId); var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
var handler = _payoutHandlers var handler = _payoutHandlers
.FindPayoutHandler(paymentMethodId); .FindPayoutHandler(paymentMethodId);
@@ -370,7 +375,7 @@ namespace BTCPayServer.Controllers
break; break;
} }
if (command == "approve-pay") if (command == "approve-pay" && !vm.HasPayoutProcessor)
{ {
goto case "pay"; goto case "pay";
} }
@@ -486,16 +491,18 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId }); return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
} }
paymentMethodId ??= paymentMethods.First().ToString();
var vm = this.ParseListQuery(new PayoutsModel var vm = this.ParseListQuery(new PayoutsModel
{ {
PaymentMethods = paymentMethods, PaymentMethods = paymentMethods,
PaymentMethodId = paymentMethodId ?? paymentMethods.First().ToString(), PaymentMethodId = paymentMethodId,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
PayoutState = payoutState, PayoutState = payoutState,
Skip = skip, Skip = skip,
Count = count Count = count,
Payouts = new List<PayoutsModel.PayoutModel>(),
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
}); });
vm.Payouts = new List<PayoutsModel.PayoutModel>();
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
var payoutRequest = var payoutRequest =
ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived)); ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived));
@@ -577,5 +584,13 @@ namespace BTCPayServer.Controllers
} }
return View(vm); return View(vm);
} }
private async Task<bool> HasPayoutProcessor(string storeId, string paymentMethodId)
{
var pmId = PaymentMethodId.Parse(paymentMethodId);
var processors = await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [pmId] });
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any();
}
} }
} }

View File

@@ -19,6 +19,7 @@ namespace BTCPayServer.Models.WalletViewModels
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; } public IEnumerable<PaymentMethodId> PaymentMethods { get; set; }
public PayoutState PayoutState { get; set; } public PayoutState PayoutState { get; set; }
public string PullPaymentName { get; set; } public string PullPaymentName { get; set; }
public bool HasPayoutProcessor { get; set; }
public class PayoutModel public class PayoutModel
{ {

View File

@@ -7,8 +7,6 @@
@model BTCPayServer.Models.WalletViewModels.PayoutsModel @model BTCPayServer.Models.WalletViewModels.PayoutsModel
@inject IEnumerable<IPayoutHandler> PayoutHandlers; @inject IEnumerable<IPayoutHandler> PayoutHandlers;
@inject PayoutProcessorService _payoutProcessorService;
@inject IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
@{ @{
var storeId = Context.GetRouteValue("storeId") as string; var storeId = Context.GetRouteValue("storeId") as string;
ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id); ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id);
@@ -24,15 +22,16 @@
return; return;
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value)); stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
} }
switch (Model.PayoutState) switch (Model.PayoutState)
{ {
case PayoutState.AwaitingApproval: case PayoutState.AwaitingApproval:
if (!Model.HasPayoutProcessor) stateActions.Add(("approve-pay", "Approve & Send"));
stateActions.Add(("approve", "Approve")); stateActions.Add(("approve", "Approve"));
stateActions.Add(("approve-pay", "Approve & Send"));
stateActions.Add(("cancel", "Cancel")); stateActions.Add(("cancel", "Cancel"));
break; break;
case PayoutState.AwaitingPayment: case PayoutState.AwaitingPayment:
stateActions.Add(("pay", "Send")); if (!Model.HasPayoutProcessor) stateActions.Add(("pay", "Send"));
stateActions.Add(("cancel", "Cancel")); stateActions.Add(("cancel", "Cancel"));
stateActions.Add(("mark-paid", "Mark as already paid")); stateActions.Add(("mark-paid", "Mark as already paid"));
break; break;
@@ -87,11 +86,7 @@
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
@if (_payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(paymentMethodId)) && !(await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() @if (!Model.HasPayoutProcessor)
{
Stores = new[] { storeId },
PaymentMethods = new[] { PaymentMethodId.Parse(Model.PaymentMethodId) }
})).Any())
{ {
<div class="alert alert-info mb-5" role="alert" permission="@Policies.CanModifyStoreSettings"> <div class="alert alert-info mb-5" role="alert" permission="@Policies.CanModifyStoreSettings">
<strong>Pro tip:</strong> There are supported but unconfigured Payout Processors for this payout payment method.<br/> <strong>Pro tip:</strong> There are supported but unconfigured Payout Processors for this payout payment method.<br/>