mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Improve documentation of Refund API in Greenfield (#4372)
This commit is contained in:
parent
3370240541
commit
ddcfa735e0
@ -132,13 +132,12 @@ namespace BTCPayServer.Client
|
||||
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,
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/refund", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PullPaymentData>(response);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
@ -9,17 +10,18 @@ namespace BTCPayServer.Client.Models
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
Fiat,
|
||||
Custom,
|
||||
NotSet
|
||||
Custom
|
||||
}
|
||||
|
||||
public class RefundInvoiceRequest
|
||||
{
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
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;
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1587,13 +1587,15 @@ namespace BTCPayServer.Tests
|
||||
// test validation that the invoice exists
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
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() {
|
||||
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
}));
|
||||
Assert.Equal("Cannot refund this invoice", apiError.Message);
|
||||
@ -1610,67 +1612,74 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
// test validation for the payment method
|
||||
var validationError = await AssertValidationError(new[] { "paymentMethod" }, async () =>
|
||||
var validationError = await AssertValidationError(new[] { "PaymentMethod" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, "fake payment method", new RefundInvoiceRequest() {
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = "fake payment method",
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
});
|
||||
Assert.Contains("paymentMethod: Please select one of the payment methods which were available for the original invoice", validationError.Message);
|
||||
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() {
|
||||
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
Assert.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() {
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.CurrentRate
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(1, pp.Amount);
|
||||
|
||||
// test RefundVariant.Fiat
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, method.PaymentMethod, new RefundInvoiceRequest() {
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Fiat,
|
||||
Name = "my test name"
|
||||
});
|
||||
Assert.Equal("USD", pp.Currency);
|
||||
Assert.Equal(false, pp.AutoApproveClaims);
|
||||
Assert.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() {
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
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() {
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 69420,
|
||||
CustomCurrency = "JPY"
|
||||
});
|
||||
Assert.Equal("JPY", pp.Currency);
|
||||
Assert.Equal(false, pp.AutoApproveClaims);
|
||||
Assert.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() {
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest() {
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.Custom,
|
||||
CustomAmount = 0.00069420m,
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.Equal(true, pp.AutoApproveClaims);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
@ -352,11 +352,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/refund")]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund")]
|
||||
public async Task<IActionResult> RefundInvoice(
|
||||
string storeId,
|
||||
string invoiceId,
|
||||
string paymentMethod,
|
||||
RefundInvoiceRequest request,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@ -377,20 +376,24 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
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)
|
||||
PaymentMethod? invoicePaymentMethod = null;
|
||||
PaymentMethodId? paymentMethodId = null;
|
||||
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(paymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
|
||||
return this.CreateValidationError(ModelState);
|
||||
invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
}
|
||||
if (invoicePaymentMethod is null)
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
}
|
||||
if (request.RefundVariant is null)
|
||||
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
|
||||
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
@ -410,6 +413,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PaymentMethodIds = new[] { paymentMethodId },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
{
|
||||
if (request.CustomAmount is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
|
||||
if (request.CustomCurrency is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
switch (request.RefundVariant)
|
||||
{
|
||||
case RefundVariant.RateThen:
|
||||
@ -431,7 +444,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
break;
|
||||
|
||||
case RefundVariant.Custom:
|
||||
if (request.CustomAmount <= 0) {
|
||||
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0)) {
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
|
||||
}
|
||||
|
||||
@ -449,13 +462,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
if (!ModelState.IsValid || request.CustomAmount is null)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = request.CustomCurrency;
|
||||
createPullPayment.Amount = request.CustomAmount;
|
||||
createPullPayment.Amount = request.CustomAmount.Value;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
|
||||
break;
|
||||
|
||||
|
@ -650,11 +650,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/refund": {
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}/refund": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Invoices",
|
||||
"Refund"
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Refund invoice",
|
||||
"parameters": [
|
||||
@ -675,19 +674,64 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "paymentMethod",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"description": "Refund invoice",
|
||||
"operationId": "Invoices_Refund",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "The payment method to refund with",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the pull payment (Default: 'Refund' followed by the invoice id)",
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the pull payment"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"type": "string",
|
||||
"description": "The payment method to use for the refund",
|
||||
"example": "BTC"
|
||||
},
|
||||
"refundVariant": {
|
||||
"type": "string",
|
||||
"description": "* `RateThen`: Refund the crypto currency price, at the rate the invoice got paid.\r\n* `CurrentRate`: Refund the crypto currency price, at the current rate.\r\n*`Fiat`: Refund the invoice currency, at the rate when the refund will be sent.\r\n*`Custom`: Specify the amount, currency, and rate of the refund. (see `customAmount` and `customCurrency`)",
|
||||
"x-enumNames": [
|
||||
"RateThen",
|
||||
"CurrentRate",
|
||||
"Fiat",
|
||||
"Custom"
|
||||
],
|
||||
"enum": [
|
||||
"RateThen",
|
||||
"CurrentRate",
|
||||
"Fiat",
|
||||
"Custom"
|
||||
]
|
||||
},
|
||||
"customAmount": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"description": "The amount to refund if the `refundVariant` is `Custom`.",
|
||||
"example": "5.00"
|
||||
},
|
||||
"customCurrency": {
|
||||
"type": "string",
|
||||
"description": "The currency to refund if the `refundVariant` is `Custom`",
|
||||
"example": "USD"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Refund invoice",
|
||||
"operationId": "Invoices_Refund",
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pull payment for refunding the invoice",
|
||||
|
Loading…
Reference in New Issue
Block a user