btcpayserver/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
Andrew Camilleri 6049fa23a7
Support pluginable rate providers (#5777)
* Support pluginable rate providers

This PR allows plugins to provide custom rate providers, that can be contextual to a store. For example, if you use the upcoming fiat offramp plugin, or the Blink plugin, you'll probably want to configure the fetch the rates from them since they are determining the actual fiat rrate to you. However, they require API keys. This PR enables these scenarios, even much more advanced ones, but for example:
* Install fiat offramp plugin
* Configure it
* You can now use the fiat offramp rate provider (no additional config steps beyond selecting the rate source from the select, or maybe the plugin would automatically set it for you once configured)

* Apply suggestions from code review

* Simplify

* Do not use BackgroundFetcherRateProvider for contextual rate prov

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-04-30 18:31:15 +09:00

681 lines
30 KiB
C#

#nullable enable
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.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NBitcoin;
using Newtonsoft.Json.Linq;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldInvoiceController : Controller
{
private readonly UIInvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository;
private readonly LinkGenerator _linkGenerator;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly RateFetcher _rateProvider;
private readonly InvoiceActivator _invoiceActivator;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService;
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PaymentMethodHandlerDictionary _handlers;
public LanguageService LanguageService { get; }
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
CurrencyNameTable currencyNameTable, RateFetcher rateProvider,
InvoiceActivator invoiceActivator,
PullPaymentHostedService pullPaymentService,
ApplicationDbContextFactory dbContextFactory,
IAuthorizationService authorizationService,
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PaymentMethodHandlerDictionary handlers)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator;
_currencyNameTable = currencyNameTable;
_networkProvider = btcPayNetworkProvider;
_rateProvider = rateProvider;
_invoiceActivator = invoiceActivator;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
_authorizationService = authorizationService;
_paymentLinkExtensions = paymentLinkExtensions;
_handlers = handlers;
LanguageService = languageService;
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[]? orderId = null, [FromQuery] string[]? status = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? startDate = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? endDate = null,
[FromQuery] string? textSearch = null,
[FromQuery] bool includeArchived = false,
[FromQuery] int? skip = null,
[FromQuery] int? take = null
)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (startDate is DateTimeOffset s &&
endDate is DateTimeOffset e &&
s > e)
{
this.ModelState.AddModelError(nameof(startDate), "startDate should not be above endDate");
this.ModelState.AddModelError(nameof(endDate), "endDate should not be below startDate");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var invoices =
await _invoiceRepository.GetInvoices(new InvoiceQuery()
{
Skip = skip,
Take = take,
StoreId = new[] { store.Id },
IncludeArchived = includeArchived,
StartDate = startDate,
EndDate = endDate,
OrderId = orderId,
Status = status,
TextSearch = textSearch
});
return Ok(invoices.Select(ToModel));
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
}
return Ok(ToModel(invoice));
}
[Authorize(Policy = Policies.CanModifyInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
}
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
return Ok();
}
[Authorize(Policy = Policies.CanModifyInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
if (result != null)
{
return Ok(ToModel(result));
}
return InvoiceNotFound();
}
[Authorize(Policy = Policies.CanCreateInvoice,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (request.Amount < 0.0m)
{
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
}
if (request.Amount > GreenfieldConstants.MaxAmount)
{
ModelState.AddModelError(nameof(request.Amount), $"The amount should less than {GreenfieldConstants.MaxAmount}.");
}
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
if (request.Checkout.PaymentMethods?.Any() is true)
{
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
{
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
"Invalid payment method", this);
}
}
}
if (request.Checkout.Expiration != null && request.Checkout.Expiration < TimeSpan.FromSeconds(30.0))
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.Expiration,
"Expiration time must be at least 30 seconds", this);
}
if (request.Checkout.PaymentTolerance != null &&
(request.Checkout.PaymentTolerance < 0 || request.Checkout.PaymentTolerance > 100))
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentTolerance,
"PaymentTolerance can only be between 0 and 100 percent", this);
}
if (request.Checkout.DefaultLanguage != null)
{
var lang = LanguageService.FindLanguage(request.Checkout.DefaultLanguage);
if (lang == null)
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.DefaultLanguage,
"The requested defaultLang does not exists, Browse the ~/misc/lang page of your BTCPay Server instance to see the list of supported languages.", this);
}
else
{
// Ensure this is good case
request.Checkout.DefaultLanguage = lang.Code;
}
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
Request.GetAbsoluteRoot());
return Ok(ToModel(invoice));
}
catch (BitpayHttpException e)
{
return this.CreateAPIError(null, e.Message);
}
}
[Authorize(Policy = Policies.CanModifyInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")]
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
MarkInvoiceStatusRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
{
ModelState.AddModelError(nameof(request.Status),
"Status can only be marked to invalid or settled within certain conditions.");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
return await GetInvoice(storeId, invoiceId);
}
[Authorize(Policy = Policies.CanModifyInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (!invoice.Archived)
{
return this.CreateAPIError("already-unarchived", "Invoice is already unarchived");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
return await GetInvoice(storeId, invoiceId);
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
}
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return InvoiceNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice?.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
{
await _invoiceActivator.ActivateInvoicePaymentMethod(invoiceId, paymentMethodId);
return Ok();
}
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund")]
public async Task<IActionResult> RefundInvoice(
string storeId,
string invoiceId,
RefundInvoiceRequest request,
CancellationToken cancellationToken = default
)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
{
return InvoiceNotFound();
}
if (invoice.StoreId != store.Id)
{
return InvoiceNotFound();
}
if (!invoice.GetInvoiceState().CanRefund())
{
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
}
PaymentPrompt? paymentPrompt = null;
PaymentMethodId? paymentMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
{
paymentPrompt = invoice.GetPaymentPrompt(paymentMethodId);
}
if (paymentPrompt is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || paymentPrompt is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var accounting = paymentPrompt.Calculate();
var cryptoPaid = accounting.Paid;
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentPrompt.Currency, invoice.Currency),
store.GetStoreBlob().GetRateRules(_networkProvider), new StoreIdRateContext(storeId),
cancellationToken
);
var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility);
var createPullPayment = new CreatePullPayment
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
Description = request.Description,
StoreId = storeId,
PaymentMethodIds = new[] { paymentMethodId },
};
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
}
if (request.SubtractPercentage is < 0 or > 100)
{
ModelState.AddModelError(nameof(request.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var appliedDivisibility = paymentPrompt.Divisibility;
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = paidAmount;
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Fiat:
appliedDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = paidCurrency;
createPullPayment.AutoApproveClaims = false;
break;
case RefundVariant.OverpaidAmount:
if (invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
{
ModelState.AddModelError(nameof(request.RefundVariant), "Invoice is not overpaid");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue;
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Custom:
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0))
{
ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
string.IsNullOrEmpty(request.CustomCurrency) ||
_currencyNameTable.GetCurrencyData(request.CustomCurrency, false) == null
)
{
ModelState.AddModelError(nameof(request.CustomCurrency), "Invalid currency");
}
if (rateResult.BidAsk is null)
{
ModelState.AddModelError(nameof(request.RefundVariant),
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
}
if (!ModelState.IsValid || request.CustomAmount is null)
{
return this.CreateValidationError(ModelState);
}
createPullPayment.Currency = request.CustomCurrency;
createPullPayment.Amount = request.CustomAmount.Value;
createPullPayment.AutoApproveClaims = paymentPrompt.Currency == request.CustomCurrency;
break;
default:
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
return this.CreateValidationError(ModelState);
}
// reduce by percentage
if (request.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (request.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
}
createPullPayment.AutoApproveClaims = createPullPayment.AutoApproveClaims && (await _authorizationService.AuthorizeAsync(User, createPullPayment.StoreId ,Policies.CanCreatePullPayments)).Succeeded;
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
await using var ctx = _dbContextFactory.CreateContext();
ctx.Refunds.Add(new RefundData
{
InvoiceDataId = invoice.Id,
PullPaymentDataId = ppId
});
await ctx.SaveChangesAsync(cancellationToken);
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp));
}
private Client.Models.PullPaymentData CreatePullPaymentData(Data.PullPaymentData pp)
{
var ppBlob = pp.GetBlob();
return new BTCPayServer.Client.Models.PullPaymentData()
{
Id = pp.Id,
StartsAt = pp.StartDate,
ExpiresAt = pp.EndDate,
Amount = ppBlob.Limit,
Name = ppBlob.Name,
Description = ppBlob.Description,
Currency = ppBlob.Currency,
Period = ppBlob.Period,
Archived = pp.Archived,
AutoApproveClaims = ppBlob.AutoApproveClaims,
BOLT11Expiration = ppBlob.BOLT11Expiration,
ViewLink = _linkGenerator.GetUriByAction(
nameof(UIPullPaymentController.ViewPullPayment),
"UIPullPayment",
new { pullPaymentId = pp.Id },
Request.Scheme,
Request.Host,
Request.PathBase)
};
}
private IActionResult InvoiceNotFound()
{
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
{
return entity.GetPaymentPrompts().Select(
prompt =>
{
_handlers.TryGetValue(prompt.PaymentMethodId, out var handler);
var accounting = prompt.Currency is not null ? prompt.Calculate() : null;
var payments = prompt.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
paymentEntity.PaymentMethodId == prompt.PaymentMethodId);
_paymentLinkExtensions.TryGetValue(prompt.PaymentMethodId, out var paymentLinkExtension);
var details = prompt.Details;
if (handler is not null && prompt.Activated)
details = JToken.FromObject(handler.ParsePaymentPromptDetails(details), handler.Serializer.ForAPI());
return new InvoicePaymentMethodDataModel
{
Activated = prompt.Activated,
PaymentMethodId = prompt.PaymentMethodId.ToString(),
Currency = prompt.Currency,
Destination = prompt.Destination,
Rate = prompt.Currency is not null ? prompt.Rate : 0m,
Due = accounting?.DueUncapped ?? 0m,
TotalPaid = accounting?.Paid ?? 0m,
PaymentMethodPaid = accounting?.PaymentMethodPaid ?? 0m,
Amount = accounting?.TotalDue ?? 0m,
PaymentMethodFee = accounting?.PaymentMethodFee ?? 0m,
PaymentLink = (prompt.Activated ? paymentLinkExtension?.GetPaymentLink(prompt, Url) : null) ?? string.Empty,
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
AdditionalData = prompt.Details
};
}).ToArray();
}
public static InvoicePaymentMethodDataModel.Payment ToPaymentModel(InvoiceEntity entity, PaymentEntity paymentEntity)
{
return new InvoicePaymentMethodDataModel.Payment()
{
Destination = paymentEntity.Destination,
Id = paymentEntity.Id,
Status = paymentEntity.Status switch
{
PaymentStatus.Processing => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
PaymentStatus.Settled => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled,
PaymentStatus.Unaccounted => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid,
_ => throw new NotSupportedException(paymentEntity.Status.ToString())
},
Fee = paymentEntity.PaymentMethodFee,
Value = paymentEntity.Value,
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}
private InvoiceData ToModel(InvoiceEntity entity)
{
return ToModel(entity, _linkGenerator, Request);
}
public static InvoiceData ToModel(InvoiceEntity entity, LinkGenerator linkGenerator, HttpRequest? request)
{
var statuses = new List<InvoiceStatus>();
var state = entity.GetInvoiceState();
if (state.CanMarkComplete())
{
statuses.Add(InvoiceStatus.Settled);
}
if (state.CanMarkInvalid())
{
statuses.Add(InvoiceStatus.Invalid);
}
return new InvoiceData()
{
StoreId = entity.StoreId,
ExpirationTime = entity.ExpirationTime,
MonitoringExpiration = entity.MonitoringExpiration,
CreatedTime = entity.InvoiceTime,
Amount = entity.Price,
Type = entity.Type,
Id = entity.Id,
CheckoutLink = request is null ? null : linkGenerator.CheckoutLink(entity.Id, request.Scheme, request.Host, request.PathBase),
Status = entity.Status.ToModernStatus(),
AdditionalStatus = entity.ExceptionStatus,
Currency = entity.Currency,
Archived = entity.Archived,
Metadata = entity.Metadata.ToJObject(),
AvailableStatusesForManualMarking = statuses.ToArray(),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
Expiration = entity.ExpirationTime - entity.InvoiceTime,
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
PaymentTolerance = entity.PaymentTolerance,
PaymentMethods =
entity.GetPaymentPrompts().Select(method => method.PaymentMethodId.ToString()).ToArray(),
DefaultPaymentMethod = entity.DefaultPaymentMethod?.ToString(),
SpeedPolicy = entity.SpeedPolicy,
DefaultLanguage = entity.DefaultLanguage,
RedirectAutomatically = entity.RedirectAutomatically,
RedirectURL = entity.RedirectURLTemplate
},
Receipt = entity.ReceiptOptions
};
}
}
}