Improve documentation of Refund API in Greenfield (#4372)

This commit is contained in:
Nicolas Dorier 2022-11-28 20:58:18 +09:00 committed by GitHub
parent 3370240541
commit ddcfa735e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 48 deletions

View File

@ -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);
}

View File

@ -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; }
}
}

View File

@ -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)]

View File

@ -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;

View File

@ -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",