mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
[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:
parent
3805b7f287
commit
4bbc7d9662
@ -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)
|
||||
{
|
||||
|
15
BTCPayServer.Client/Models/PayPaymentRequestRequest.cs
Normal file
15
BTCPayServer.Client/Models/PayPaymentRequestRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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)]
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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 });
|
||||
|
@ -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; }
|
||||
|
@ -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))
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user