[Greenfield] Can create an invoice for a payment request via Greenfield (#4243)

* [Greenfield] Can create an invoice for a payment request via Greenfield

* Add allowPendingInvoiceReuse so payment request invoices can be reused

* Add PayPaymentRequest to the LocalBTCPayServerClient

* Allow amount to be specified if same as PR amount
This commit is contained in:
Nicolas Dorier 2022-11-02 18:41:19 +09:00 committed by GitHub
parent 3805b7f287
commit 4bbc7d9662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 354 additions and 64 deletions

View File

@ -37,6 +37,20 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<Client.Models.InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (storeId is null)
throw new ArgumentNullException(nameof(storeId));
if (paymentRequestId is null)
throw new ArgumentNullException(nameof(paymentRequestId));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<Client.Models.InvoiceData>(response);
}
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PayPaymentRequestRequest
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public bool? AllowPendingInvoiceReuse { get; set; }
}
}

View File

@ -1169,25 +1169,93 @@ namespace BTCPayServer.Tests
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
Assert.DoesNotContain(paymentRequest.Id,
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
//let's test some payment stuff
var archivedPrId = paymentRequest.Id;
//let's test some payment stuff with the UI
await user.RegisterDerivationSchemeAsync("BTC");
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
async Task Pay(string invoiceId, bool partialPayment = false)
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
TestLogs.LogInformation($"Paying invoice {invoiceId}");
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
TestLogs.LogInformation($"Paying address {invoice.BitcoinAddress}");
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
}
await Pay(invoiceId);
//Same thing, but with the API
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
var paidPrId = paymentTestPaymentRequest.Id;
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
await Pay(invoiceData.Id);
// Let's tests some unhappy path
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title"
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m });
Assert.Equal(0.04m, invoiceData.Amount);
var firstPaymentId = invoiceData.Id;
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title",
ExpiryDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(1.0)
});
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
{
Amount = 0.1m,
AllowCustomPaymentAmounts = true,
Currency = "BTC",
Title = "Payment test title",
ExpiryDate = null
});
await Pay(firstPaymentId, true);
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
Assert.Equal(0.06m, invoiceData.Amount);
Assert.Equal("BTC", invoiceData.Currency);
var expectedInvoiceId = invoiceData.Id;
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = true });
Assert.Equal(expectedInvoiceId, invoiceData.Id);
var notExpectedInvoiceId = invoiceData.Id;
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = false });
Assert.NotEqual(notExpectedInvoiceId, invoiceData.Id);
}
[Fact(Timeout = TestTimeout)]

View File

@ -1,18 +1,22 @@
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.PaymentRequest;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Controllers.Greenfield
@ -22,14 +26,26 @@ namespace BTCPayServer.Controllers.Greenfield
[EnableCors(CorsPolicies.All)]
public class GreenfieldPaymentRequestsController : ControllerBase
{
private readonly InvoiceRepository _InvoiceRepository;
private readonly UIInvoiceController _invoiceController;
private readonly PaymentRequestRepository _paymentRequestRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly LinkGenerator _linkGenerator;
public GreenfieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
CurrencyNameTable currencyNameTable)
public GreenfieldPaymentRequestsController(
InvoiceRepository invoiceRepository,
UIInvoiceController invoiceController,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
CurrencyNameTable currencyNameTable,
LinkGenerator linkGenerator)
{
_InvoiceRepository = invoiceRepository;
_invoiceController = invoiceController;
_paymentRequestRepository = paymentRequestRepository;
PaymentRequestService = paymentRequestService;
_currencyNameTable = currencyNameTable;
_linkGenerator = linkGenerator;
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -56,6 +72,62 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(pr.First()));
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay")]
public async Task<IActionResult> PayPaymentRequest(string storeId, string paymentRequestId, [FromBody] PayPaymentRequestRequest pay, CancellationToken cancellationToken)
{
var pr = await this.PaymentRequestService.GetPaymentRequest(paymentRequestId);
if (pr is null || pr.StoreId != storeId)
return PaymentRequestNotFound();
var amount = pay?.Amount;
if (amount.HasValue && amount.Value <= 0)
{
ModelState.AddModelError(nameof(pay.Amount), "The amount should be more than 0");
}
if (amount.HasValue && !pr.AllowCustomPaymentAmounts && amount.Value != pr.AmountDue)
{
ModelState.AddModelError(nameof(pay.Amount), "This payment request doesn't allow custom payment amount");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (pr.Archived)
{
return this.CreateAPIError("archived", "You cannot pay an archived payment request");
}
if (pr.AmountDue <= 0)
{
return this.CreateAPIError("already-paid", "This payment request is already paid");
}
if (pr.ExpiryDate.HasValue && DateTime.UtcNow >= pr.ExpiryDate)
{
return this.CreateAPIError("expired", "This payment request is expired");
}
if (pay?.AllowPendingInvoiceReuse is true)
{
if (pr.Invoices.GetReusableInvoice(amount)?.Id is string invoiceId)
{
var inv = await _InvoiceRepository.GetInvoice(invoiceId);
return Ok(GreenfieldInvoiceController.ToModel(inv, _linkGenerator, Request));
}
}
try
{
var invoice = await _invoiceController.CreatePaymentRequestInvoice(pr, amount, this.StoreData, Request, cancellationToken);
return Ok(GreenfieldInvoiceController.ToModel(invoice, _linkGenerator, Request));
}
catch (BitpayHttpException e)
{
return this.CreateAPIError(null, e.Message);
}
}
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
@ -97,6 +169,9 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(FromModel(pr));
}
public Data.StoreData StoreData => HttpContext.GetStoreData();
public PaymentRequestService PaymentRequestService { get; }
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View File

@ -592,6 +592,12 @@ namespace BTCPayServer.Controllers.Greenfield
HandleActionResult(await GetController<GreenfieldPaymentRequestsController>().ArchivePaymentRequest(storeId, paymentRequestId));
}
public override async Task<InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
{
return GetFromActionResult<InvoiceData>(
await GetController<GreenfieldPaymentRequestsController>().PayPaymentRequest(storeId, paymentRequestId, request, token));
}
public override async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{

View File

@ -5,21 +5,25 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
@ -168,6 +172,35 @@ namespace BTCPayServer.Controllers
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
}
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(ViewPaymentRequestViewModel pr, decimal? amount, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
{
if (pr.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(pr.AmountDue, amount.Value);
else
amount = pr.AmountDue;
var redirectUrl = _linkGenerator.PaymentRequestLink(pr.Id, request.Scheme, request.Host, request.PathBase);
var invoiceMetadata =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(pr.Id),
PaymentRequestId = pr.Id,
BuyerEmail = pr.Email
};
var invoiceRequest =
new CreateInvoiceRequest
{
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
{
var storeBlob = store.GetStoreBlob();

View File

@ -35,7 +35,6 @@ namespace BTCPayServer.Controllers
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly InvoiceRepository _InvoiceRepository;
private readonly LinkGenerator _linkGenerator;
public UIPaymentRequestController(
UIInvoiceController invoiceController,
@ -44,8 +43,7 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator)
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -54,7 +52,6 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_Currencies = currencies;
_InvoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator;
}
[BitpayAPIConstraint(false)]
@ -213,14 +210,7 @@ namespace BTCPayServer.Controllers
return BadRequest("Payment Request has expired");
}
var stateAllowedToDisplay = new HashSet<InvoiceState>
{
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.None),
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.PaidPartial),
};
var currentInvoice = result
.Invoices
.FirstOrDefault(invoice => stateAllowedToDisplay.Contains(invoice.State));
var currentInvoice = result.Invoices.GetReusableInvoice(amount);
if (currentInvoice != null)
{
if (redirectToInvoice)
@ -231,38 +221,9 @@ namespace BTCPayServer.Controllers
return Ok(currentInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
var pr = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null, cancellationToken);
var blob = pr.GetBlob();
var store = pr.StoreData;
try
{
var redirectUrl = _linkGenerator.PaymentRequestLink(payReqId, Request.Scheme, Request.Host, Request.PathBase);
var invoiceMetadata =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(payReqId),
PaymentRequestId = payReqId,
BuyerEmail = result.Email
};
var invoiceRequest =
new CreateInvoiceRequest
{
Metadata = invoiceMetadata.ToJObject(),
Currency = blob.Currency,
Amount = amount.Value,
Checkout = { RedirectURL = redirectUrl }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(payReqId) };
var newInvoice = await _InvoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags, cancellationToken);
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(result, amount, this.GetCurrentStore(), Request, cancellationToken);
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = newInvoice.Id });

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
@ -130,7 +131,32 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public List<PaymentRequestInvoice> Invoices { get; set; } = new List<PaymentRequestInvoice>();
#nullable enable
public class InvoiceList : List<PaymentRequestInvoice>
{
static HashSet<InvoiceState> stateAllowedToDisplay = new HashSet<InvoiceState>
{
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.None),
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.PaidPartial),
};
public InvoiceList()
{
}
public InvoiceList(IEnumerable<PaymentRequestInvoice> collection) : base(collection)
{
}
public PaymentRequestInvoice? GetReusableInvoice(decimal? amount)
{
return this
.Where(i => amount is null || amount.Value == i.Amount)
.FirstOrDefault(invoice => stateAllowedToDisplay.Contains(invoice.State));
}
}
#nullable restore
public InvoiceList Invoices { get; set; } = new InvoiceList();
public DateTime LastUpdated { get; set; }
public CurrencyData CurrencyData { get; set; }
public string AmountCollectedFormatted { get; set; }

View File

@ -100,7 +100,7 @@ namespace BTCPayServer.PaymentRequest
AnyPendingInvoice = pendingInvoice != null,
PendingInvoiceHasPayments = pendingInvoice != null &&
pendingInvoice.ExceptionStatus != InvoiceExceptionStatus.None,
Invoices = invoices.Select(entity =>
Invoices = new ViewPaymentRequestViewModel.InvoiceList(invoices.Select(entity =>
{
var state = entity.GetInvoiceState();
var payments = entity
@ -153,8 +153,7 @@ namespace BTCPayServer.PaymentRequest
Payments = payments
};
})
.Where(invoice => invoice != null)
.ToList()
.Where(invoice => invoice != null))
};
}

View File

@ -206,7 +206,7 @@
},
"security": [
{
"API_Key": [ "btcpay.store.canmodifypaymentrequests"],
"API_Key": [ "btcpay.store.canmodifypaymentrequests" ],
"Basic": []
}
]
@ -278,6 +278,99 @@
}
]
}
},
"/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay": {
"post": {
"tags": [
"Payment Requests"
],
"summary": "Create a new invoice for the payment request",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": { "type": "string" }
},
{
"name": "paymentRequestId",
"in": "path",
"required": true,
"description": "The payment request to create",
"schema": { "type": "string" }
}
],
"description": "Create a new invoice for the payment request, or reuse an existing one",
"requestBody": {
"description": "Invoice creation request",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"amount": {
"type": "string",
"format": "decimal",
"minimum": 0,
"exclusiveMinimum": true,
"description": "The amount of the invoice. If `null` or `unspecified`, it will be set to the payment request's due amount. Note that the payment's request `allowCustomPaymentAmounts` must be `true`, or a 422 error will be sent back.'",
"nullable": true,
"example": "0.1"
},
"allowPendingInvoiceReuse": {
"type": "boolean",
"nullable": true,
"default": false,
"description": "If `true`, this endpoint will not necessarily create a new invoice, and instead attempt to give back a pending one for this payment request."
}
}
}
}
}
},
"responses": {
"200": {
"description": "A new invoice",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceData"
}
}
}
},
"422": {
"description": "Unable to validate the request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"400": {
"description": "Wellknown error codes are: `archived`, `already-paid`, `expired`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
},
"security": [
{
"API_Key": [
"btcpay.store.canviewpaymentrequests"
],
"Basic": []
}
]
}
}
},
"components": {
@ -320,7 +413,7 @@
}
]
},
"PaymentRequestBaseData":{
"PaymentRequestBaseData": {
"type": "object",
"additionalProperties": false,
"properties": {
@ -329,12 +422,12 @@
"format": "decimal",
"minimum": 0,
"exclusiveMinimum": true,
"description": "The amount of the payment request",
"description": "The amount of the payment request",
"nullable": false
},
"title": {
"type": "string",
"description": "The title of the payment request",
"description": "The title of the payment request",
"nullable": false
},
"currency": {
@ -358,7 +451,7 @@
"expiryDate": {
"description": "The expiry date of the payment request",
"nullable": true,
"allOf": [ {"$ref": "#/components/schemas/UnixTimestamp"}]
"allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ]
},
"embeddedCSS": {
"type": "string",