mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
Add Greenfield invoice refund endpoint (#4238)
* Add Greenfield invoice refund endpoint See discussion here: https://github.com/btcpayserver/btcpayserver/discussions/4181 * add test * add docs
This commit is contained in:
parent
420954ed00
commit
425d70f261
@ -128,5 +128,19 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PullPaymentData> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
string paymentMethod,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/refund", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PullPaymentData>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
25
BTCPayServer.Client/Models/RefundInvoiceRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum RefundVariant
|
||||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
Fiat,
|
||||
Custom,
|
||||
NotSet
|
||||
}
|
||||
|
||||
public class RefundInvoiceRequest
|
||||
{
|
||||
public string? Name { get; set; } = null;
|
||||
public string? Description { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant RefundVariant { get; set; } = RefundVariant.NotSet;
|
||||
public decimal CustomAmount { get; set; } = 0;
|
||||
public string? CustomCurrency { get; set; } = null;
|
||||
}
|
||||
}
|
@ -1561,6 +1561,118 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanRefundInvoice()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
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();
|
||||
var amount = method.Amount;
|
||||
Assert.Equal(amount, method.Due);
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due)
|
||||
);
|
||||
});
|
||||
|
||||
// test validation that the invoice exists
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
|
||||
// test validation error for when invoice is not yet in the state in which it can be refunded
|
||||
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
}));
|
||||
Assert.Equal("Cannot refund this invoice", apiError.Message);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Processing);
|
||||
});
|
||||
|
||||
// need to set the status to the one in which we can actually refund the invoice
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() {
|
||||
Status = InvoiceStatus.Settled
|
||||
});
|
||||
|
||||
// test validation for the payment method
|
||||
var validationError = await AssertValidationError(new[] { "paymentMethod" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, "fake payment method", new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
Assert.Contains("paymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
|
||||
// test RefundVariant.RateThen
|
||||
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
Assert.Equal(pp.Name, $"Refund {invoice.Id}");
|
||||
|
||||
// test RefundVariant.CurrentRate
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
|
||||
// test RefundVariant.Fiat
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.Fiat,
|
||||
Name = "my test name"
|
||||
});
|
||||
Assert.Equal("USD", pp.Currency);
|
||||
Assert.Equal(false, pp.AutoApproveClaims);
|
||||
Assert.Equal(5000, pp.Amount);
|
||||
Assert.Equal("my test name", pp.Name);
|
||||
|
||||
// test RefundVariant.Custom
|
||||
validationError = await AssertValidationError(new[] { "CustomAmount", "CustomCurrency" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
});
|
||||
});
|
||||
Assert.Contains("CustomAmount: Amount must be greater than 0", validationError.Message);
|
||||
Assert.Contains("CustomCurrency: Invalid currency", validationError.Message);
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 69420,
|
||||
CustomCurrency = "JPY"
|
||||
});
|
||||
Assert.Equal("JPY", pp.Currency);
|
||||
Assert.Equal(false, pp.AutoApproveClaims);
|
||||
Assert.Equal(69420, pp.Amount);
|
||||
|
||||
// should auto-approve if currencies match
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 0.00069420m,
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceTests()
|
||||
|
@ -2,14 +2,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Rating;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -32,12 +37,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly RateFetcher _rateProvider;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
|
||||
public LanguageService LanguageService { get; }
|
||||
|
||||
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
|
||||
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
|
||||
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
@ -45,6 +57,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_networkProvider = networkProvider;
|
||||
_rateProvider = rateProvider;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
|
||||
@ -333,6 +350,162 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/refund")]
|
||||
public async Task<IActionResult> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
string paymentMethod,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
{
|
||||
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethod);
|
||||
var invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
if (invoicePaymentMethod == null)
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(paymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency),
|
||||
store.GetStoreBlob().GetRateRules(_networkProvider),
|
||||
cancellationToken
|
||||
);
|
||||
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||
var createPullPayment = new HostedServices.CreatePullPayment()
|
||||
{
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = new[] { paymentMethodId },
|
||||
};
|
||||
|
||||
switch (request.RefundVariant)
|
||||
{
|
||||
case RefundVariant.RateThen:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.CurrentRate:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.Fiat:
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = paidCurrency;
|
||||
createPullPayment.AutoApproveClaims = false;
|
||||
break;
|
||||
|
||||
case RefundVariant.Custom:
|
||||
if (request.CustomAmount <= 0) {
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
|
||||
}
|
||||
|
||||
if (
|
||||
string.IsNullOrEmpty(request.CustomCurrency) ||
|
||||
_currencyNameTable.GetCurrencyData(request.CustomCurrency, false) == null
|
||||
)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.RefundVariant),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = request.CustomCurrency;
|
||||
createPullPayment.Amount = request.CustomAmount;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
|
||||
break;
|
||||
|
||||
default:
|
||||
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken))!.CurrentRefundId = ppId;
|
||||
ctx.Refunds.Add(new RefundData
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
PullPaymentDataId = ppId
|
||||
});
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
|
||||
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
|
||||
{
|
||||
var ppBlob = pp.GetBlob();
|
||||
return new BTCPayServer.Client.Models.PullPaymentData()
|
||||
{
|
||||
Id = pp.Id,
|
||||
StartsAt = pp.StartDate,
|
||||
ExpiresAt = pp.EndDate,
|
||||
Amount = ppBlob.Limit,
|
||||
Name = ppBlob.Name,
|
||||
Description = ppBlob.Description,
|
||||
Currency = ppBlob.Currency,
|
||||
Period = ppBlob.Period,
|
||||
Archived = pp.Archived,
|
||||
AutoApproveClaims = ppBlob.AutoApproveClaims,
|
||||
BOLT11Expiration = ppBlob.BOLT11Expiration,
|
||||
ViewLink = _linkGenerator.GetUriByAction(
|
||||
nameof(UIPullPaymentController.ViewPullPayment),
|
||||
"UIPullPayment",
|
||||
new { pullPaymentId = pp.Id },
|
||||
Request.Scheme,
|
||||
Request.Host,
|
||||
Request.PathBase)
|
||||
};
|
||||
}
|
||||
|
||||
private IActionResult InvoiceNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
|
||||
|
@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = CanRefund(invoiceState),
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
|
||||
@ -234,16 +234,6 @@ namespace BTCPayServer.Controllers
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
return network == null ? null : paymentMethodId.PaymentType.GetTransactionLink(network, txId);
|
||||
}
|
||||
bool CanRefund(InvoiceState invoiceState)
|
||||
{
|
||||
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
|
||||
invoiceState.Status == InvoiceStatusLegacy.Complete ||
|
||||
(invoiceState.Status == InvoiceStatusLegacy.Expired &&
|
||||
(invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
|
||||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
|
||||
invoiceState.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
|
||||
invoiceState.Status == InvoiceStatusLegacy.Invalid;
|
||||
}
|
||||
|
||||
[HttpGet("invoices/{invoiceId}/refund")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
@ -262,7 +252,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is null && GetUserId() is null)
|
||||
return NotFound();
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
if (invoice.CurrentRefund?.PullPaymentDataId is string ppId && !invoice.CurrentRefund.PullPaymentData.Archived)
|
||||
{
|
||||
@ -318,7 +308,7 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
return NotFound();
|
||||
|
||||
var store = GetCurrentStore();
|
||||
|
@ -835,6 +835,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
(Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
|
||||
}
|
||||
|
||||
public bool CanRefund()
|
||||
{
|
||||
return Status == InvoiceStatusLegacy.Confirmed ||
|
||||
Status == InvoiceStatusLegacy.Complete ||
|
||||
(Status == InvoiceStatusLegacy.Expired &&
|
||||
(ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
|
||||
ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
|
||||
ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
|
||||
Status == InvoiceStatusLegacy.Invalid;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Status, ExceptionStatus);
|
||||
|
@ -649,6 +649,79 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/refund": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Invoices",
|
||||
"Refund"
|
||||
],
|
||||
"summary": "Refund invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The invoice to refund",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "paymentMethod",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The payment method to refund with",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Refund invoice",
|
||||
"operationId": "Invoices_Refund",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pull payment for refunding the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PullPaymentData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when refunding the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to refund the invoice"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
Loading…
Reference in New Issue
Block a user