diff --git a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs index 64b493e2d..96e2a6d51 100644 --- a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs +++ b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs @@ -37,6 +37,20 @@ namespace BTCPayServer.Client await HandleResponse(response); } + public virtual async Task 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(response); + } + public virtual async Task CreatePaymentRequest(string storeId, CreatePaymentRequestRequest request, CancellationToken token = default) { diff --git a/BTCPayServer.Client/Models/PayPaymentRequestRequest.cs b/BTCPayServer.Client/Models/PayPaymentRequestRequest.cs new file mode 100644 index 000000000..8ff3da686 --- /dev/null +++ b/BTCPayServer.Client/Models/PayPaymentRequestRequest.cs @@ -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; } + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 292323ae6..865953447 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -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(Assert.IsType(await user.GetController() .PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value); - var invoice = user.BitPay.GetInvoice(invoiceId); - await tester.WaitForEvent(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(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)] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs index 3a16d8497..ac345fe98 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs @@ -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 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)] diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index ba33a7c92..03bb363dc 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -592,6 +592,12 @@ namespace BTCPayServer.Controllers.Greenfield HandleActionResult(await GetController().ArchivePaymentRequest(storeId, paymentRequestId)); } + public override async Task PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default) + { + return GetFromActionResult( + await GetController().PayPaymentRequest(storeId, paymentRequestId, request, token)); + } + public override async Task CreatePaymentRequest(string storeId, CreatePaymentRequestRequest request, CancellationToken token = default) { diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index f4ef3257d..046e58868 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -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 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 { PaymentRequestRepository.GetInternalTag(pr.Id) }; + return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken); + } + internal async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List? additionalTags = null, CancellationToken cancellationToken = default, Action? entityManipulator = null) { var storeBlob = store.GetStoreBlob(); diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs index b0e456c8f..0d89db423 100644 --- a/BTCPayServer/Controllers/UIPaymentRequestController.cs +++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs @@ -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 - { - 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 { 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 }); diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index 09de19b23..a150e15a4 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -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 Invoices { get; set; } = new List(); + +#nullable enable + public class InvoiceList : List + { + static HashSet stateAllowedToDisplay = new HashSet + { + new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.None), + new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.PaidPartial), + }; + public InvoiceList() + { + + } + public InvoiceList(IEnumerable 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; } diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index e1083d090..b07abad6a 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -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)) }; } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json index f984ddce9..b8de4ddc3 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json @@ -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",