diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs new file mode 100644 index 000000000..04c7acada --- /dev/null +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -0,0 +1,142 @@ +using System; +using BTCPayServer.JsonConverters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class CreateInvoiceRequest + { + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Amount + { + get; + set; + } + + public string Currency + { + get; + set; + } + + public ProductInformation Metadata { get; set; } + + public BuyerInformation Customer { get; set; } = new BuyerInformation(); + + public CheckoutOptions Checkout { get; set; } = new CheckoutOptions(); + + public class CheckoutOptions + { + [JsonConverter(typeof(StringEnumConverter))] + public SpeedPolicy? SpeedPolicy { get; set; } + + public string[] PaymentMethods { get; set; } + public bool? RedirectAutomatically { get; set; } + public string RedirectUri { get; set; } + public Uri WebHook { get; set; } + + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset? ExpirationTime { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public double? PaymentTolerance { get; set; } + } + + public class BuyerInformation + { + [JsonProperty(PropertyName = "buyerName")] + public string BuyerName + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerEmail")] + public string BuyerEmail + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerCountry")] + public string BuyerCountry + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerZip")] + public string BuyerZip + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerState")] + public string BuyerState + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerCity")] + public string BuyerCity + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerAddress2")] + public string BuyerAddress2 + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerAddress1")] + public string BuyerAddress1 + { + get; + set; + } + + [JsonProperty(PropertyName = "buyerPhone")] + public string BuyerPhone + { + get; + set; + } + } + + public class ProductInformation + { + public string OrderId { get; set; } + public string PosData { get; set; } + + public string ItemDesc + { + get; + set; + } + + public string ItemCode + { + get; + set; + } + + public bool Physical + { + get; + set; + } + + public decimal? TaxIncluded + { + get; + set; + } + } + } +} diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs new file mode 100644 index 000000000..8e447fbf6 --- /dev/null +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using BTCPayServer.JsonConverters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class InvoiceData : CreateInvoiceRequest + { + public string Id { get; set; } + public Dictionary PaymentMethodData { get; set; } + + public class PaymentMethodDataModel + { + public string Destination { get; set; } + public string PaymentLink { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Rate { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal PaymentMethodPaid { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal TotalPaid { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Due { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Amount { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal NetworkFee { get; set; } + + public List Payments { get; set; } + + public class Payment + { + public string Id { get; set; } + + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTime ReceivedDate { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Value { get; set; } + + [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + public decimal Fee { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public PaymentStatus Status { get; set; } + + public string Destination { get; set; } + + + + public enum PaymentStatus + { + Invalid, + AwaitingConfirmation, + AwaitingCompletion, + Complete + } + } + } + + } +} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 94c7f19f4..9d939cea3 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -14,6 +14,7 @@ namespace BTCPayServer.Client public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:"; public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings"; + public const string CanViewInvoices = "btcpay.store.canviewinvoices"; public const string CanCreateInvoice = "btcpay.store.cancreateinvoice"; public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests"; public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests"; @@ -26,6 +27,7 @@ namespace BTCPayServer.Client { get { + yield return CanViewInvoices; yield return CanCreateInvoice; yield return CanModifyServerSettings; yield return CanModifyStoreSettings; @@ -153,6 +155,8 @@ namespace BTCPayServer.Client return true; switch (subpolicy) { + case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings: case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile: diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f0eced4c0..5b5930b98 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -53,6 +53,7 @@ using Newtonsoft.Json.Schema; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; +using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation; using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; namespace BTCPayServer.Tests diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs new file mode 100644 index 000000000..f97be7892 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -0,0 +1,270 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Payments; +using BTCPayServer.Security; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Validation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NBitcoin; +using NBitpayClient; +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 InvoiceController _invoiceController; + private readonly InvoiceRepository _invoiceRepository; + + public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository) + { + _invoiceController = invoiceController; + _invoiceRepository = invoiceRepository; + } + + [Authorize(Policy = Policies.CanViewInvoices, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + public async Task GetInvoices(string storeId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {StoreId = new[] {store.Id}}); + + return Ok(invoices.Select(ToModel)); + } + + + [Authorize(Policy = Policies.CanViewInvoices, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + public async Task GetInvoice(string storeId, string invoiceId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice.StoreId != store.Id) + { + return NotFound(); + } + + return Ok(ToModel(invoice)); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + public async Task ArchiveInvoice(string storeId, string invoiceId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice.StoreId != store.Id) + { + return NotFound(); + } + + await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true); + return Ok(); + } + + [Authorize(Policy = Policies.CanCreateInvoice, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/invoices")] + public async Task CreateInvoice(string storeId, CreateInvoiceRequest request) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + if (request.Amount < 0.0m) + { + ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more."); + } + + 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 (!string.IsNullOrEmpty(request.Customer.BuyerEmail) && + !EmailValidator.IsEmail(request.Customer.BuyerEmail)) + { + request.AddModelError(invoiceRequest => invoiceRequest.Customer.BuyerEmail, "Invalid email address", + this); + } + + if (request.Checkout.ExpirationTime != null && request.Checkout.ExpirationTime < DateTime.Now) + { + request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExpirationTime, + "Expiration time must be in the future", 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 (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + + var invoice = await _invoiceController.CreateInvoiceCoreRaw(FromModel(request), store, + Request.GetAbsoluteUri("")); + return Ok(ToModel(invoice)); + } + + public InvoiceData ToModel(InvoiceEntity entity) + { + return new InvoiceData() + { + Amount = entity.ProductInformation.Price, + Id = entity.Id, + Currency = entity.ProductInformation.Currency, + Metadata = + new CreateInvoiceRequest.ProductInformation() + { + Physical = entity.ProductInformation.Physical, + ItemCode = entity.ProductInformation.ItemCode, + ItemDesc = entity.ProductInformation.ItemDesc, + OrderId = entity.OrderId, + PosData = entity.PosData, + TaxIncluded = entity.ProductInformation.TaxIncluded + }, + Customer = new CreateInvoiceRequest.BuyerInformation() + { + BuyerAddress1 = entity.BuyerInformation.BuyerAddress1, + BuyerAddress2 = entity.BuyerInformation.BuyerAddress2, + BuyerCity = entity.BuyerInformation.BuyerCity, + BuyerCountry = entity.BuyerInformation.BuyerCountry, + BuyerEmail = entity.BuyerInformation.BuyerEmail, + BuyerName = entity.BuyerInformation.BuyerName, + BuyerPhone = entity.BuyerInformation.BuyerPhone, + BuyerState = entity.BuyerInformation.BuyerState, + BuyerZip = entity.BuyerInformation.BuyerZip + }, + Checkout = new CreateInvoiceRequest.CheckoutOptions() + { + ExpirationTime = entity.ExpirationTime, + PaymentTolerance = entity.PaymentTolerance, + PaymentMethods = + entity.GetPaymentMethods().Select(method => method.GetId().ToString()).ToArray(), + RedirectAutomatically = entity.RedirectAutomatically, + RedirectUri = entity.RedirectURL.ToString(), + SpeedPolicy = entity.SpeedPolicy, + WebHook = entity.NotificationURL + }, + PaymentMethodData = entity.GetPaymentMethods().ToDictionary(method => method.GetId().ToString(), + method => + { + var accounting = method.Calculate(); + var details = method.GetPaymentMethodDetails(); + var payments = method.ParentEntity.GetPayments().Where(paymentEntity => + paymentEntity.GetPaymentMethodId() == method.GetId()); + + return new InvoiceData.PaymentMethodDataModel() + { + Destination = details.GetPaymentDestination(), + Rate = method.Rate, + Due = accounting.Due.ToDecimal(MoneyUnit.BTC), + TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC), + PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), + Amount = accounting.Due.ToDecimal(MoneyUnit.BTC), + NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC), + PaymentLink = + method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due, + Request.GetAbsoluteRoot()), + Payments = payments.Select(paymentEntity => + { + var data = paymentEntity.GetCryptoPaymentData(); + return new InvoiceData.PaymentMethodDataModel.Payment() + { + Destination = data.GetDestination(), + Id = data.GetPaymentId(), + Status = !paymentEntity.Accounted + ? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus.Invalid + : data.PaymentCompleted(paymentEntity) + ? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus.Complete + : data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) + ? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus + .AwaitingCompletion + : InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus + .AwaitingConfirmation, + Fee = paymentEntity.NetworkFee, + Value = data.GetValue(), + ReceivedDate = paymentEntity.ReceivedTime.DateTime + }; + }).ToList() + }; + }) + }; + } + + public Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity) + { + return new Models.CreateInvoiceRequest() + { + Buyer = new Buyer() + { + country = entity.Customer.BuyerCountry, + email = entity.Customer.BuyerEmail, + phone = entity.Customer.BuyerPhone, + zip = entity.Customer.BuyerZip, + Address1 = entity.Customer.BuyerAddress1, + Address2 = entity.Customer.BuyerAddress2, + City = entity.Customer.BuyerCity, + Name = entity.Customer.BuyerName, + State = entity.Customer.BuyerState, + }, + Currency = entity.Currency, + Physical = entity.Metadata.Physical, + Price = entity.Amount, + Refundable = true, + ExtendedNotifications = true, + FullNotifications = true, + RedirectURL = entity.Checkout.RedirectUri, + RedirectAutomatically = entity.Checkout.RedirectAutomatically, + ItemCode = entity.Metadata.ItemCode, + ItemDesc = entity.Metadata.ItemDesc, + ExpirationTime = entity.Checkout.ExpirationTime, + TransactionSpeed = entity.Checkout.SpeedPolicy?.ToString(), + PaymentCurrencies = entity.Checkout.PaymentMethods, + TaxIncluded = entity.Metadata.TaxIncluded, + OrderId = entity.Metadata.OrderId, + NotificationURL = entity.Checkout.RedirectUri, + PosData = entity.Metadata.PosData + }; + } + } +} diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 4afb92f57..df6d6a5db 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -30,6 +30,8 @@ using NBitcoin; using NBitpayClient; using NBXplorer; using Newtonsoft.Json.Linq; +using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; +using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 8f4f0a7e9..cc52715e4 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -23,7 +23,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using NBitpayClient; using Newtonsoft.Json; +using BuyerInformation = BTCPayServer.Services.Invoices.BuyerInformation; using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; +using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers @@ -72,7 +74,16 @@ namespace BTCPayServer.Controllers } - internal async Task> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) + internal async Task> CreateInvoiceCore(CreateInvoiceRequest invoice, + StoreData store, string serverUrl, List additionalTags = null, + CancellationToken cancellationToken = default) + { + var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken); + var resp = entity.EntityToDTO(); + return new DataWrapper(resp) {Facade = "pos/invoice"}; + } + + internal async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) { invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; InvoiceLogs logs = new InvoiceLogs(); @@ -233,8 +244,7 @@ namespace BTCPayServer.Controllers await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs); }); _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created)); - var resp = entity.EntityToDTO(); - return new DataWrapper(resp) { Facade = "pos/invoice" }; + return entity; } private Task WhenAllFetched(InvoiceLogs logs, Dictionary> fetchingByCurrencyPair) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index a0499d0b5..ba5ce104e 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -363,6 +363,8 @@ namespace BTCPayServer.Controllers {BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")}, {$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")}, + {BTCPayServer.Client.Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")}, + {$"{BTCPayServer.Client.Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view invoices on the selected stores.")}, {BTCPayServer.Client.Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")}, {$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")}, {BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")}, diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 4ba1f46ef..0d9f912e8 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using BTCPayServer.Client.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Services.Invoices; using NBitcoin; using Newtonsoft.Json; +using BuyerInformation = BTCPayServer.Services.Invoices.BuyerInformation; +using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation; namespace BTCPayServer.Models.InvoicingModels { diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 30948bc94..ed7249341 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -465,7 +465,7 @@ namespace BTCPayServer.Services.Invoices { var minerInfo = new MinerFeeInfo(); minerInfo.TotalFee = accounting.NetworkFee.Satoshi; - minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate .GetFee(1).Satoshi; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); cryptoInfo.PaymentUrls = new InvoicePaymentUrls() diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 8bd644c52..59eb21398 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; using Encoders = NBitcoin.DataEncoders.Encoders; +using InvoiceData = BTCPayServer.Data.InvoiceData; namespace BTCPayServer.Services.Invoices {