mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Fix: Refunds through API were ignoring BOLT11 expiration at store level (#6644)
This commit is contained in:
@@ -30,6 +30,12 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<StoreData>("api/v1/stores", request, HttpMethod.Post, token);
|
||||
}
|
||||
|
||||
public async Task<StoreData> UpdateStore(string storeId, StoreData request, CancellationToken token = default)
|
||||
{
|
||||
if (request is null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
return await UpdateStore(storeId, Newtonsoft.Json.JsonConvert.DeserializeObject<UpdateStoreRequest>(Newtonsoft.Json.JsonConvert.SerializeObject(request)), token);
|
||||
}
|
||||
public virtual async Task<StoreData> UpdateStore(string storeId, UpdateStoreRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan DisplayExpirationTimer { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Days))]
|
||||
[JsonProperty("refundBOLT11Expiration", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan RefundBOLT11Expiration { get; set; } = TimeSpan.FromDays(30);
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
|
||||
|
||||
@@ -2293,6 +2293,13 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
var store = await client.GetStore(user.StoreId);
|
||||
Assert.Equal(TimeSpan.FromDays(30.0), store.RefundBOLT11Expiration);
|
||||
store.RefundBOLT11Expiration = TimeSpan.FromDays(1);
|
||||
await client.UpdateStore(store.Id, store);
|
||||
store = await client.GetStore(user.StoreId);
|
||||
Assert.Equal(TimeSpan.FromDays(1.0), store.RefundBOLT11Expiration);
|
||||
|
||||
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" });
|
||||
var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
var method = methods.First();
|
||||
@@ -2354,6 +2361,7 @@ namespace BTCPayServer.Tests
|
||||
PayoutMethodId = method.PaymentMethodId,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal(pp.BOLT11Expiration, TimeSpan.FromDays(1));
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
|
||||
@@ -376,13 +376,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
cancellationToken
|
||||
);
|
||||
var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility);
|
||||
var createPullPayment = new CreatePullPayment
|
||||
var createPullPayment = new CreatePullPaymentRequest
|
||||
{
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PayoutMethods = new[] { payoutMethodId },
|
||||
PayoutMethods = new[] { payoutMethodId.ToString() },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
@@ -479,8 +477,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
|
||||
}
|
||||
|
||||
createPullPayment.AutoApproveClaims = createPullPayment.AutoApproveClaims && (await _authorizationService.AuthorizeAsync(User, createPullPayment.StoreId ,Policies.CanCreatePullPayments)).Succeeded;
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
|
||||
createPullPayment.AutoApproveClaims = createPullPayment.AutoApproveClaims && (await _authorizationService.AuthorizeAsync(User, storeId ,Policies.CanCreatePullPayments)).Succeeded;
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(store, createPullPayment);
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
|
||||
|
||||
@@ -126,7 +126,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
var supported = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
|
||||
request.PayoutMethods ??= supported.Select(s => s.ToString()).ToArray();
|
||||
if (request.PayoutMethods is not null)
|
||||
{
|
||||
for (int i = 0; i < request.PayoutMethods.Length; i++)
|
||||
{
|
||||
var pmi = request.PayoutMethods[i] is string pm ? PayoutMethodId.TryParse(pm) : null;
|
||||
@@ -139,9 +140,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "At least one payout method is required");
|
||||
}
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
|
||||
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(HttpContext.GetStoreData(), request);
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Runtime.Internal;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@@ -18,6 +19,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@@ -174,6 +176,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Website = data.StoreWebsite,
|
||||
Archived = data.Archived,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
RefundBOLT11Expiration = storeBlob.RefundBOLT11Expiration,
|
||||
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend,
|
||||
CssUrl = storeBlob.CssUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
|
||||
LogoUrl = storeBlob.LogoUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
|
||||
@@ -258,6 +261,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.ApplyBrandColorToBackend = restModel.ApplyBrandColorToBackend;
|
||||
blob.LogoUrl = restModel.LogoUrl is null ? null : UnresolvedUri.Create(restModel.LogoUrl);
|
||||
blob.CssUrl = restModel.CssUrl is null ? null : UnresolvedUri.Create(restModel.CssUrl);
|
||||
blob.RefundBOLT11Expiration = restModel.RefundBOLT11Expiration;
|
||||
blob.PaymentSoundUrl = restModel.PaymentSoundUrl is null ? null : UnresolvedUri.Create(restModel.PaymentSoundUrl);
|
||||
if (restModel.AutoDetectLanguage.HasValue)
|
||||
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
|
||||
@@ -295,6 +299,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Name), "DefaultPaymentMethod is invalid");
|
||||
}
|
||||
if (request.RefundBOLT11Expiration < TimeSpan.FromDays(0) ||
|
||||
request.RefundBOLT11Expiration > TimeSpan.FromDays(365 * 10))
|
||||
ModelState.AddModelError(nameof(request.RefundBOLT11Expiration), "refundBOLT11Expiration should be between 0 and 36500");
|
||||
|
||||
if (string.IsNullOrEmpty(request.Name))
|
||||
ModelState.AddModelError(nameof(request.Name), "Name is missing");
|
||||
|
||||
@@ -342,7 +342,7 @@ namespace BTCPayServer.Controllers
|
||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
RateRules rules;
|
||||
RateResult rateResult;
|
||||
CreatePullPayment createPullPayment;
|
||||
CreatePullPaymentRequest createPullPayment;
|
||||
|
||||
var pmis = _payoutHandlers.GetSupportedPayoutMethods(store);
|
||||
if (!pmis.Contains(pmi))
|
||||
@@ -414,12 +414,10 @@ namespace BTCPayServer.Controllers
|
||||
return View("_RefundModal", model);
|
||||
|
||||
case RefundSteps.SelectRate:
|
||||
createPullPayment = new CreatePullPayment
|
||||
createPullPayment = new CreatePullPaymentRequest
|
||||
{
|
||||
Name = StringLocalizer["Refund {0}", invoice.Id],
|
||||
PayoutMethods = new[] { pmi },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
PayoutMethods = new[] { pmi.ToString() }
|
||||
};
|
||||
var authorizedForAutoApprove = (await
|
||||
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
|
||||
@@ -529,7 +527,7 @@ namespace BTCPayServer.Controllers
|
||||
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, ppDivisibility);
|
||||
}
|
||||
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(store, createPullPayment);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Store > Payouts.",
|
||||
|
||||
@@ -147,14 +147,13 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
model.AutoApproveClaims = model.AutoApproveClaims && (await
|
||||
_authorizationService.AuthorizeAsync(User, storeId, Policies.CanCreatePullPayments)).Succeeded;
|
||||
await _pullPaymentService.CreatePullPayment(new CreatePullPayment
|
||||
await _pullPaymentService.CreatePullPayment(CurrentStore, new CreatePullPaymentRequest
|
||||
{
|
||||
Name = model.Name,
|
||||
Description = model.Description,
|
||||
Amount = model.Amount,
|
||||
Currency = model.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethods = selectedPaymentMethodIds,
|
||||
PayoutMethods = selectedPaymentMethodIds.Select(p => p.ToString()).ToArray(),
|
||||
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
|
||||
AutoApproveClaims = model.AutoApproveClaims
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
@@ -33,20 +34,6 @@ using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CreatePullPayment
|
||||
{
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public PayoutMethodId[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
public TimeSpan? BOLT11Expiration { get; set; }
|
||||
}
|
||||
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
@@ -110,26 +97,14 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
||||
public async Task<string> CreatePullPayment(Data.StoreData store, CreatePullPaymentRequest create)
|
||||
{
|
||||
if (request.PayoutMethods.Length == 0)
|
||||
var supported = this._handlers.GetSupportedPayoutMethods(store);
|
||||
create.PayoutMethods ??= supported.Select(s => s.ToString()).ToArray();
|
||||
create.PayoutMethods = create.PayoutMethods.Where(pm => _handlers.Support(PayoutMethodId.Parse(pm))).ToArray();
|
||||
if (create.PayoutMethods.Length == 0)
|
||||
throw new InvalidOperationException("request.PayoutMethods should have at least one payout method");
|
||||
return CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethods = request.PayoutMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
}
|
||||
public async Task<string> CreatePullPayment(CreatePullPayment create)
|
||||
{
|
||||
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
if (create.Amount <= 0.0m)
|
||||
throw new ArgumentException("Amount out of bound", nameof(create));
|
||||
@@ -140,7 +115,7 @@ namespace BTCPayServer.HostedServices
|
||||
: DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1.0);
|
||||
o.EndDate = create.ExpiresAt is DateTimeOffset date2 ? new DateTimeOffset?(date2) : null;
|
||||
o.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
o.StoreId = create.StoreId;
|
||||
o.StoreId = store.Id;
|
||||
o.Currency = create.Currency;
|
||||
o.Limit = create.Amount;
|
||||
|
||||
@@ -148,7 +123,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Name = create.Name ?? string.Empty,
|
||||
Description = create.Description ?? string.Empty,
|
||||
SupportedPayoutMethods = create.PayoutMethods,
|
||||
SupportedPayoutMethods = create.PayoutMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = create.AutoApproveClaims,
|
||||
View = new PullPaymentBlob.PullPaymentView
|
||||
{
|
||||
@@ -156,7 +131,7 @@ namespace BTCPayServer.HostedServices
|
||||
Description = create.Description ?? string.Empty,
|
||||
Email = null
|
||||
},
|
||||
BOLT11Expiration = create.BOLT11Expiration ?? TimeSpan.FromDays(30.0)
|
||||
BOLT11Expiration = create.BOLT11Expiration ?? store.GetStoreBlob().RefundBOLT11Expiration
|
||||
});
|
||||
ctx.PullPayments.Add(o);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
@@ -126,6 +126,15 @@
|
||||
"format": "seconds",
|
||||
"description": "A span of times in seconds"
|
||||
},
|
||||
"TimeSpanDays": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TimeSpan"
|
||||
}
|
||||
],
|
||||
"format": "days",
|
||||
"description": "A span of times in days"
|
||||
},
|
||||
"TimeSpanMinutes": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -518,6 +518,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"refundBOLT11Expiration": {
|
||||
"default": 30,
|
||||
"minimum": 0,
|
||||
"maximum": 3650,
|
||||
"description": "The minimum expiry of BOLT11 invoices accepted for refunds by default. (in days)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TimeSpanDays"
|
||||
}
|
||||
]
|
||||
},
|
||||
"displayExpirationTimer": {
|
||||
"default": 300,
|
||||
"minimum": 60,
|
||||
|
||||
Reference in New Issue
Block a user