From 34e76494e33e3bb61932946097b733cae2efb1b4 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 22 Jul 2020 13:58:41 +0200 Subject: [PATCH 01/16] GreenField: Invoice API --- .../Models/CreateInvoiceRequest.cs | 142 +++++++++ BTCPayServer.Client/Models/InvoiceData.cs | 70 +++++ BTCPayServer.Client/Permissions.cs | 4 + BTCPayServer.Tests/UnitTest1.cs | 1 + .../GreenField/InvoiceController.cs | 270 ++++++++++++++++++ .../Controllers/InvoiceController.UI.cs | 2 + BTCPayServer/Controllers/InvoiceController.cs | 16 +- .../Controllers/ManageController.APIKeys.cs | 2 + .../InvoicingModels/InvoiceDetailsModel.cs | 3 + .../Services/Invoices/InvoiceEntity.cs | 2 +- .../Services/Invoices/InvoiceRepository.cs | 1 + 11 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 BTCPayServer.Client/Models/CreateInvoiceRequest.cs create mode 100644 BTCPayServer.Client/Models/InvoiceData.cs create mode 100644 BTCPayServer/Controllers/GreenField/InvoiceController.cs 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 { From cb5601c68b5a9af03ba0c59bc00636a1f068d463 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 24 Jul 2020 08:13:21 +0200 Subject: [PATCH 02/16] unarchive endpoint + formatting --- .../Models/CreateInvoiceRequest.cs | 90 ++++--------------- .../Models/UpdateInvoiceRequest.cs | 7 ++ .../GreenField/InvoiceController.cs | 31 +++++-- .../Services/Invoices/InvoiceRepository.cs | 6 +- 4 files changed, 48 insertions(+), 86 deletions(-) create mode 100644 BTCPayServer.Client/Models/UpdateInvoiceRequest.cs diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 04c7acada..103bb5415 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -8,17 +8,9 @@ namespace BTCPayServer.Client.Models public class CreateInvoiceRequest { [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] - public decimal Amount - { - get; - set; - } + public decimal Amount { get; set; } - public string Currency - { - get; - set; - } + public string Currency { get; set; } public ProductInformation Metadata { get; set; } @@ -46,67 +38,31 @@ namespace BTCPayServer.Client.Models public class BuyerInformation { [JsonProperty(PropertyName = "buyerName")] - public string BuyerName - { - get; - set; - } + public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] - public string BuyerEmail - { - get; - set; - } + public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] - public string BuyerCountry - { - get; - set; - } + public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] - public string BuyerZip - { - get; - set; - } + public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] - public string BuyerState - { - get; - set; - } + public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] - public string BuyerCity - { - get; - set; - } + public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] - public string BuyerAddress2 - { - get; - set; - } + public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] - public string BuyerAddress1 - { - get; - set; - } + public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] - public string BuyerPhone - { - get; - set; - } + public string BuyerPhone { get; set; } } public class ProductInformation @@ -114,29 +70,13 @@ namespace BTCPayServer.Client.Models public string OrderId { get; set; } public string PosData { get; set; } - public string ItemDesc - { - get; - set; - } + public string ItemDesc { get; set; } - public string ItemCode - { - get; - set; - } + public string ItemCode { get; set; } - public bool Physical - { - get; - set; - } + public bool Physical { get; set; } - public decimal? TaxIncluded - { - get; - set; - } + public decimal? TaxIncluded { get; set; } } } } diff --git a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs new file mode 100644 index 000000000..dea03c3b1 --- /dev/null +++ b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Client.Models +{ + public class UpdateInvoiceRequest + { + public bool Archived { get; set; } + } +} diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index f97be7892..0caf6db2c 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client; +using BTCPayServer.Client.Models; using BTCPayServer.Payments; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; @@ -9,7 +10,6 @@ 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; @@ -42,7 +42,11 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {StoreId = new[] {store.Id}}); + var invoices = + await _invoiceRepository.GetInvoices(new InvoiceQuery() + { + StoreId = new[] {store.Id}, IncludeArchived = false + }); return Ok(invoices.Select(ToModel)); } @@ -79,13 +83,7 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice.StoreId != store.Id) - { - return NotFound(); - } - - await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true); + await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId); return Ok(); } @@ -145,6 +143,21 @@ namespace BTCPayServer.Controllers.GreenField return Ok(ToModel(invoice)); } + [Authorize(Policy = Policies.CanModifyStoreSettings, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + public async Task UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + await _invoiceRepository.ToggleInvoiceArchival(invoiceId, request.Archived, storeId); + return await GetInvoice(storeId, invoiceId); + } + public InvoiceData ToModel(InvoiceEntity entity) { return new InvoiceData() diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 59eb21398..0deadd243 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -428,12 +428,14 @@ retry: } } - public async Task ToggleInvoiceArchival(string invoiceId, bool archived) + public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null) { using (var context = _ContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null || invoiceData.Archived == archived) + if (invoiceData == null || invoiceData.Archived == archived || + (storeId != null && + invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) return; invoiceData.Archived = archived; await context.SaveChangesAsync().ConfigureAwait(false); From d96bd15b3b21ac74a2354b4443c817cf655839bf Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 24 Jul 2020 09:40:37 +0200 Subject: [PATCH 03/16] add status and refactor --- BTCPayServer.Client/Models/InvoiceData.cs | 4 ++ .../Models/InvoiceExceptionStatus.cs | 12 ++++ BTCPayServer.Client/Models/InvoiceStatus.cs | 12 ++++ .../Models/UpdateInvoiceRequest.cs | 4 +- BTCPayServer.Tests/PayJoinTests.cs | 1 + BTCPayServer.Tests/PaymentRequestTests.cs | 1 + .../GreenField/InvoiceController.cs | 55 ++++++++++++++++++- .../Controllers/InvoiceController.UI.cs | 6 +- BTCPayServer/Controllers/InvoiceController.cs | 2 +- .../Controllers/PaymentRequestController.cs | 8 ++- BTCPayServer/Events/InvoiceEvent.cs | 20 ++++++- .../InvoiceNotificationManager.cs | 1 + BTCPayServer/HostedServices/InvoiceWatcher.cs | 15 ++--- BTCPayServer/Hosting/BTCPayServerServices.cs | 2 +- .../Models/InvoicingModels/InvoicesModel.cs | 1 + .../PaymentRequest/PaymentRequestService.cs | 2 + .../Payments/Bitcoin/NBXplorerListener.cs | 2 +- .../Payments/Lightning/LightningListener.cs | 2 +- .../PayJoin/PayJoinEndpointController.cs | 2 +- .../Monero/Services/MoneroListener.cs | 2 +- BTCPayServer/Services/Apps/AppService.cs | 2 + .../Services/Invoices/InvoiceEntity.cs | 19 +------ .../Services/Invoices/InvoiceRepository.cs | 54 ++++++++++++------ 23 files changed, 167 insertions(+), 62 deletions(-) create mode 100644 BTCPayServer.Client/Models/InvoiceExceptionStatus.cs create mode 100644 BTCPayServer.Client/Models/InvoiceStatus.cs diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 8e447fbf6..e2838fb02 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -9,6 +9,10 @@ namespace BTCPayServer.Client.Models public class InvoiceData : CreateInvoiceRequest { public string Id { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public InvoiceStatus Status { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public InvoiceExceptionStatus ExceptionStatus { get; set; } public Dictionary PaymentMethodData { get; set; } public class PaymentMethodDataModel diff --git a/BTCPayServer.Client/Models/InvoiceExceptionStatus.cs b/BTCPayServer.Client/Models/InvoiceExceptionStatus.cs new file mode 100644 index 000000000..e526fde3a --- /dev/null +++ b/BTCPayServer.Client/Models/InvoiceExceptionStatus.cs @@ -0,0 +1,12 @@ +namespace BTCPayServer.Client.Models +{ + public enum InvoiceExceptionStatus + { + None, + PaidLate, + PaidPartial, + Marked, + Invalid, + PaidOver + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Models/InvoiceStatus.cs b/BTCPayServer.Client/Models/InvoiceStatus.cs new file mode 100644 index 000000000..a3d866acd --- /dev/null +++ b/BTCPayServer.Client/Models/InvoiceStatus.cs @@ -0,0 +1,12 @@ +namespace BTCPayServer.Client.Models +{ + public enum InvoiceStatus + { + New, + Paid, + Expired, + Invalid, + Complete, + Confirmed + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs index dea03c3b1..5b3ebf983 100644 --- a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs @@ -2,6 +2,8 @@ namespace BTCPayServer.Client.Models { public class UpdateInvoiceRequest { - public bool Archived { get; set; } + public bool? Archived { get; set; } + public InvoiceStatus? Status { get; set; } + public string Email { get; set; } } } diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 270d8c3e5..41825f703 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Events; diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs index 4ef4f0659..df844addd 100644 --- a/BTCPayServer.Tests/PaymentRequestTests.cs +++ b/BTCPayServer.Tests/PaymentRequestTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.PaymentRequest; diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 0caf6db2c..ee730829b 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; +using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; @@ -154,16 +155,64 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - await _invoiceRepository.ToggleInvoiceArchival(invoiceId, request.Archived, storeId); + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice.StoreId != store.Id) + { + return NotFound(); + } + + if (request.Archived.HasValue) + { + if (request.Archived.Value && !invoice.Archived) + { + ModelState.AddModelError(nameof(request.Archived), + "You can only archive an invoice via HTTP DELETE."); + } + else if (!request.Archived.Value && invoice.Archived) + { + await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId); + } + } + + if (request.Status != null) + { + if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status.Value)) + { + ModelState.AddModelError(nameof(request.Status), + "Status can only be marked to invalid or complete within certain conditions."); + } + } + + if (request.Email != null) + { + if (!EmailValidator.IsEmail(request.Email)) + { + request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address", + this); + } + else if (!string.IsNullOrEmpty(invoice.BuyerInformation.BuyerEmail)) + { + request.AddModelError(invoiceRequest => invoiceRequest.Email, "Email address already set", + this); + } + + await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() {Email = request.Email}); + } + + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + return await GetInvoice(storeId, invoiceId); } - public InvoiceData ToModel(InvoiceEntity entity) + private InvoiceData ToModel(InvoiceEntity entity) { return new InvoiceData() { Amount = entity.ProductInformation.Price, Id = entity.Id, + Status = entity.Status, + ExceptionStatus = entity.ExceptionStatus, Currency = entity.ProductInformation.Currency, Metadata = new CreateInvoiceRequest.ProductInformation() @@ -244,7 +293,7 @@ namespace BTCPayServer.Controllers.GreenField }; } - public Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity) + private Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity) { return new Models.CreateInvoiceRequest() { diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index df6d6a5db..9fcb90689 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -778,14 +778,12 @@ namespace BTCPayServer.Controllers } if (newState == "invalid") { - await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId); - _EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid)); + await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Invalid); model.StatusString = new InvoiceState("invalid", "marked").ToString(); } else if (newState == "complete") { - await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId); - _EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted)); + await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Complete); model.StatusString = new InvoiceState("complete", "marked").ToString(); } diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index cc52715e4..c8da6d15b 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -243,7 +243,7 @@ namespace BTCPayServer.Controllers } await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs); }); - _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created)); + _EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created)); return entity; } diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index ebbcd790b..3e198c14d 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Filters; @@ -19,6 +20,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Routing; +using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; +using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; +using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers { @@ -303,9 +307,7 @@ namespace BTCPayServer.Controllers foreach (var invoice in invoices) { - await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id); - _EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, - InvoiceEvent.MarkedInvalid)); + await _InvoiceRepository.MarkInvoiceStatus(invoice.Id, InvoiceStatus.Invalid); } if (redirect) diff --git a/BTCPayServer/Events/InvoiceEvent.cs b/BTCPayServer/Events/InvoiceEvent.cs index b55f7384d..ede5028ca 100644 --- a/BTCPayServer/Events/InvoiceEvent.cs +++ b/BTCPayServer/Events/InvoiceEvent.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using BTCPayServer.Services.Invoices; namespace BTCPayServer.Events @@ -15,11 +16,26 @@ namespace BTCPayServer.Events public const string FailedToConfirm = "invoice_failedToConfirm"; public const string Confirmed = "invoice_confirmed"; public const string Completed = "invoice_completed"; + + public static Dictionary EventCodes = new Dictionary() + { + {Created, 1001}, + {ReceivedPayment, 1002}, + {PaidInFull, 1003}, + {Expired, 1004}, + {Confirmed, 1005}, + {Completed, 1006}, + {MarkedInvalid, 1008}, + {FailedToConfirm, 1013}, + {PaidAfterExpiration, 1009}, + {ExpiredPaidPartial, 2000}, + {MarkedCompleted, 2008}, + }; - public InvoiceEvent(InvoiceEntity invoice, int code, string name) + public InvoiceEvent(InvoiceEntity invoice, string name) { Invoice = invoice; - EventCode = code; + EventCode = EventCodes[name]; Name = name; } diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 83133f511..5502104c6 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Events; using BTCPayServer.Payments; using BTCPayServer.Services; diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index c3a94114b..af3101f72 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Events; using BTCPayServer.Logging; using BTCPayServer.Services.Invoices; @@ -65,9 +66,9 @@ namespace BTCPayServer.HostedServices await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = InvoiceStatus.Expired; - context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired)); if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) - context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial)); } var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); @@ -81,7 +82,7 @@ namespace BTCPayServer.HostedServices { if (invoice.Status == InvoiceStatus.New) { - context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull)); invoice.Status = InvoiceStatus.Paid; invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None; await _InvoiceRepository.UnaffectAddress(invoice.Id); @@ -90,7 +91,7 @@ namespace BTCPayServer.HostedServices else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate; - context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidAfterExpiration)); context.MarkDirty(); } } @@ -136,7 +137,7 @@ namespace BTCPayServer.HostedServices (confirmedAccounting.Paid < accounting.MinimumTotalDue)) { await _InvoiceRepository.UnaffectAddress(invoice.Id); - context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm)); invoice.Status = InvoiceStatus.Invalid; context.MarkDirty(); } @@ -144,7 +145,7 @@ namespace BTCPayServer.HostedServices { await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = InvoiceStatus.Confirmed; - context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed)); context.MarkDirty(); } } @@ -154,7 +155,7 @@ namespace BTCPayServer.HostedServices var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)); if (completedAccounting.Paid >= accounting.MinimumTotalDue) { - context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed)); + context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed)); invoice.Status = InvoiceStatus.Complete; context.MarkDirty(); } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 04fb29eeb..ebcaa9fe6 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -96,7 +96,7 @@ namespace BTCPayServer.Hosting var dbpath = Path.Combine(opts.DataDir, "InvoiceDB"); if (!Directory.Exists(dbpath)) Directory.CreateDirectory(dbpath); - return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService()); + return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService(), o.GetService()); }); services.AddSingleton(); services.TryAddSingleton(); diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 1e109a706..001ddd973 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using BTCPayServer.Client.Models; using BTCPayServer.Services.Invoices; namespace BTCPayServer.Models.InvoicingModels diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index da6f7cbaf..e743fcbae 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Payments; @@ -9,6 +10,7 @@ using BTCPayServer.Services.Invoices; using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.SignalR; +using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; namespace BTCPayServer.PaymentRequest { diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 243ab0f55..0f61f54f5 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -406,7 +406,7 @@ namespace BTCPayServer.Payments.Bitcoin invoice.SetPaymentMethod(paymentMethod); } wallet.InvalidateCache(strategy); - _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); + _Aggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); return invoice; } public async Task StopAsync(CancellationToken cancellationToken) diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index ac09e1650..4c8da4d52 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -329,7 +329,7 @@ namespace BTCPayServer.Payments.Lightning { var invoice = await invoiceRepository.GetInvoice(invoiceId); if (invoice != null) - _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); + _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } return payment != null; } diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 56666857e..98e57abbf 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -454,7 +454,7 @@ namespace BTCPayServer.Payments.PayJoin $"The original transaction has already been accounted")); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); - _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); + _eventAggregator.Publish(new InvoiceEvent(invoice,InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = new WalletId(invoice.StoreId, network.CryptoCode), diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 95d9e0fb8..ee17ce646 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -143,7 +143,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services } _eventAggregator.Publish( - new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); + new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 2a1f92abd..9ad30f2a4 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Payments; @@ -20,6 +21,7 @@ using NUglify.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using static BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel; +using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Services.Apps { diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index ed7249341..72730ed96 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -604,24 +604,7 @@ namespace BTCPayServer.Services.Invoices } } - public enum InvoiceStatus - { - New, - Paid, - Expired, - Invalid, - Complete, - Confirmed - } - public enum InvoiceExceptionStatus - { - None, - PaidLate, - PaidPartial, - Marked, - Invalid, - PaidOver - } + public class InvoiceState { static readonly Dictionary _StringToInvoiceStatus; diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 0deadd243..c1a2f2f59 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Data; +using BTCPayServer.Events; using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; @@ -37,11 +38,12 @@ namespace BTCPayServer.Services.Invoices } private readonly ApplicationDbContextFactory _ContextFactory; + private readonly EventAggregator _eventAggregator; private readonly BTCPayNetworkProvider _Networks; private readonly CustomThreadPool _IndexerThread; public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, - BTCPayNetworkProvider networks) + BTCPayNetworkProvider networks, EventAggregator eventAggregator) { int retryCount = 0; retry: @@ -53,6 +55,7 @@ retry: _IndexerThread = new CustomThreadPool(1, "Invoice Indexer"); _ContextFactory = contextFactory; _Networks = networks; + _eventAggregator = eventAggregator; } public InvoiceEntity CreateNewInvoice() @@ -441,29 +444,44 @@ retry: await context.SaveChangesAsync().ConfigureAwait(false); } } - public async Task UpdatePaidInvoiceToInvalid(string invoiceId) + public async Task MarkInvoiceStatus(string invoiceId, InvoiceStatus status) { using (var context = _ContextFactory.CreateContext()) { var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid()) - return; - invoiceData.Status = "invalid"; - invoiceData.ExceptionStatus = "marked"; - await context.SaveChangesAsync().ConfigureAwait(false); - } - } - public async Task UpdatePaidInvoiceToComplete(string invoiceId) - { - using (var context = _ContextFactory.CreateContext()) - { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete()) - return; - invoiceData.Status = "complete"; - invoiceData.ExceptionStatus = "marked"; + if (invoiceData == null) + { + return false; + } + + string eventName; + switch (status) + { + case InvoiceStatus.Complete: + if (!invoiceData.GetInvoiceState().CanMarkComplete()) + { + return false; + } + + eventName = InvoiceEvent.MarkedCompleted; + break; + case InvoiceStatus.Invalid: + if (!invoiceData.GetInvoiceState().CanMarkInvalid()) + { + return false; + } + eventName = InvoiceEvent.MarkedInvalid; + break; + default: + return false; + } + invoiceData.Status =status.ToString().ToLowerInvariant(); + invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant(); + _eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName)); await context.SaveChangesAsync().ConfigureAwait(false); } + + return true; } public async Task GetInvoice(string id, bool inludeAddressData = false) { From 7ca2df9fcc89b150a305ea307b06d9e54b3cb844 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 24 Jul 2020 10:53:31 +0200 Subject: [PATCH 04/16] rename converter --- .../Models/CreateInvoiceRequest.cs | 4 ++-- BTCPayServer.Client/Models/InvoiceData.cs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 103bb5415..3987ff1c9 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models { public class CreateInvoiceRequest { - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } public string Currency { get; set; } @@ -31,7 +31,7 @@ namespace BTCPayServer.Client.Models [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset? ExpirationTime { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public double? PaymentTolerance { get; set; } } diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index e2838fb02..f4a6f0373 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -20,22 +20,22 @@ namespace BTCPayServer.Client.Models public string Destination { get; set; } public string PaymentLink { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Rate { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal PaymentMethodPaid { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal TotalPaid { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Due { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal NetworkFee { get; set; } public List Payments { get; set; } @@ -47,10 +47,10 @@ namespace BTCPayServer.Client.Models [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTime ReceivedDate { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Value { get; set; } - [JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))] + [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Fee { get; set; } [JsonConverter(typeof(StringEnumConverter))] From 8f4f87cd8f7c727a9b5a3d60102df2ffa110ce69 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Fri, 24 Jul 2020 11:00:36 +0200 Subject: [PATCH 05/16] Update BTCPayServer.Client/Models/InvoiceData.cs Co-authored-by: Dennis Reimann --- BTCPayServer.Client/Models/InvoiceData.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index f4a6f0373..83cb597e4 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -57,8 +57,6 @@ namespace BTCPayServer.Client.Models public PaymentStatus Status { get; set; } public string Destination { get; set; } - - public enum PaymentStatus { From 4917f32574c178f46480a36ec7be5d573e7b4a58 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Fri, 24 Jul 2020 11:00:45 +0200 Subject: [PATCH 06/16] Update BTCPayServer/Controllers/GreenField/InvoiceController.cs Co-authored-by: Dennis Reimann --- .../GreenField/InvoiceController.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index ee730829b..ba1433f2b 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -214,16 +214,15 @@ namespace BTCPayServer.Controllers.GreenField Status = entity.Status, ExceptionStatus = entity.ExceptionStatus, 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 - }, + 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, From 41bebfedc445d759d7d411b5426d6d8c0e5825ae Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 24 Jul 2020 12:46:46 +0200 Subject: [PATCH 07/16] add swagger docs --- .../BTCPayServerClient.Invoices.cs | 61 ++ .../BTCPayServerClient.PaymentRequests.cs | 5 +- BTCPayServer.Tests/ApiKeysTests.cs | 2 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 91 +++ .../GreenField/InvoiceController.cs | 28 +- .../Services/Invoices/InvoiceRepository.cs | 19 +- .../ListInvoicesPaymentsPartial.cshtml | 3 +- .../swagger/v1/swagger.template.invoices.json | 712 ++++++++++++++++++ .../v1/swagger.template.payment-requests.json | 4 +- 9 files changed, 907 insertions(+), 18 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.Invoices.cs create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs new file mode 100644 index 000000000..aae9ddea7 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task> GetInvoices(string storeId, bool includeArchived = false, + CancellationToken token = default) + { + var response = + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices", + new Dictionary() {{nameof(includeArchived), includeArchived}}), token); + return await HandleResponse>(response); + } + + public virtual async Task GetInvoice(string storeId, string invoiceId, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}"), token); + return await HandleResponse(response); + } + + public virtual async Task ArchiveInvoice(string storeId, string invoiceId, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}", + method: HttpMethod.Delete), token); + await HandleResponse(response); + } + + public virtual async Task CreateInvoice(string storeId, + CreateInvoiceRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public virtual async Task UpdateInvoice(string storeId, string invoiceId, + UpdateInvoiceRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}", bodyPayload: request, + method: HttpMethod.Put), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs index feeea0151..eff1f46f1 100644 --- a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs +++ b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs @@ -10,10 +10,13 @@ namespace BTCPayServer.Client public partial class BTCPayServerClient { public virtual async Task> GetPaymentRequests(string storeId, + bool includeArchived = false, CancellationToken token = default) { var response = - await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token); + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests", + new Dictionary() {{nameof(includeArchived), includeArchived}}), token); return await HandleResponse>(response); } diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 4150027aa..dd4cb8972 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -89,7 +89,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click(); //there should be a store already by default in the dropdown - var dropdown = s.Driver.FindElement(By.Name("PermissionValues[2].SpecificStores[0]")); + var dropdown = s.Driver.FindElement(By.Name("PermissionValues[3].SpecificStores[0]")); var option = dropdown.FindElement(By.TagName("option")); var storeId = option.GetAttribute("value"); option.Click(); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 1262e55e6..7c7f57ebf 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -731,6 +731,97 @@ namespace BTCPayServer.Tests }); } } + + + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task InvoiceTests() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + await user.MakeAdmin(); + var client = await user.CreateClient(Policies.Unrestricted); + var viewOnly = await user.CreateClient(Policies.CanViewInvoices); + + //create + + //validation errors + await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () => + { + await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions(){ PaymentTolerance = -2, PaymentMethods = new []{"jasaas_sdsad"}}}); + }); + + await AssertHttpError(403, async () => + { + await viewOnly.CreateInvoice(user.StoreId, + new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 }); + }); + await user.RegisterDerivationSchemeAsync("BTC"); + var newInvoice = await client.CreateInvoice(user.StoreId, + new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = new CreateInvoiceRequest.ProductInformation(){ ItemCode = "testitem"}}); + + //list + var invoices = await viewOnly.GetInvoices(user.StoreId); + + Assert.NotNull(invoices); + Assert.Single(invoices); + Assert.Equal(newInvoice.Id, invoices.First().Id); + + //get payment request + var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); + Assert.Equal(newInvoice.Metadata.ItemCode, invoice.Metadata.ItemCode); + + //update + await AssertHttpError(403, async () => + { + await viewOnly.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + { + Email = "j@g.com" + }); + }); + await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + { + Email = "j@g.com" + }); + invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); + Assert.Equal(invoice.Customer.BuyerEmail, "j@g.com"); + + await AssertValidationError(new[] { nameof(UpdateInvoiceRequest.Email), nameof(UpdateInvoiceRequest.Archived),nameof(UpdateInvoiceRequest.Status) }, async () => + { + await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + { + Email = "j@g2.com", + Archived = true, + Status = InvoiceStatus.Complete + }); + }); + + + //archive + await AssertHttpError(403, async () => + { + await viewOnly.ArchiveInvoice(user.StoreId, invoice.Id); + }); + + await client.ArchiveInvoice(user.StoreId, invoice.Id); + Assert.DoesNotContain(invoice.Id, + (await client.GetInvoices(user.StoreId)).Select(data => data.Id)); + + //unarchive + await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + { + Archived = false, + }); + Assert.NotNull(await client.GetInvoice(user.StoreId,invoice.Id)); + + } + } + + [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index ba1433f2b..e0dd86743 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -34,8 +34,8 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] - public async Task GetInvoices(string storeId) + [HttpGet("~/api/v1/stores/{storeId}/invoices")] + public async Task GetInvoices(string storeId, bool includeArchived = false) { var store = HttpContext.GetStoreData(); if (store == null) @@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers.GreenField var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() { - StoreId = new[] {store.Id}, IncludeArchived = false + StoreId = new[] {store.Id}, IncludeArchived = includeArchived }); return Ok(invoices.Select(ToModel)); @@ -104,6 +104,11 @@ namespace BTCPayServer.Controllers.GreenField ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more."); } + if (string.IsNullOrEmpty(request.Currency)) + { + ModelState.AddModelError(nameof(request.Currency), "Currency is required"); + } + if (request.Checkout.PaymentMethods?.Any() is true) { for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++) @@ -139,14 +144,21 @@ namespace BTCPayServer.Controllers.GreenField if (!ModelState.IsValid) return this.CreateValidationError(ModelState); - var invoice = await _invoiceController.CreateInvoiceCoreRaw(FromModel(request), store, - Request.GetAbsoluteUri("")); - return Ok(ToModel(invoice)); + try + { + var invoice = await _invoiceController.CreateInvoiceCoreRaw(FromModel(request), store, + Request.GetAbsoluteUri("")); + return Ok(ToModel(invoice)); + } + catch (BitpayHttpException e) + { + return this.CreateAPIError(null, e.Message); + } } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] + [HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] public async Task UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request) { var store = HttpContext.GetStoreData(); @@ -242,7 +254,7 @@ namespace BTCPayServer.Controllers.GreenField PaymentMethods = entity.GetPaymentMethods().Select(method => method.GetId().ToString()).ToArray(), RedirectAutomatically = entity.RedirectAutomatically, - RedirectUri = entity.RedirectURL.ToString(), + RedirectUri = entity.RedirectURL?.ToString(), SpeedPolicy = entity.SpeedPolicy, WebHook = entity.NotificationURL }, diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index c1a2f2f59..d65ba35ca 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -438,7 +438,7 @@ retry: var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); if (invoiceData == null || invoiceData.Archived == archived || (storeId != null && - invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) + !invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) return; invoiceData.Archived = archived; await context.SaveChangesAsync().ConfigureAwait(false); @@ -448,12 +448,13 @@ retry: { using (var context = _ContextFactory.CreateContext()) { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + var invoiceData = await GetInvoiceRaw(invoiceId); if (invoiceData == null) { return false; } + context.Attach(invoiceData); string eventName; switch (status) { @@ -475,15 +476,23 @@ retry: default: return false; } - invoiceData.Status =status.ToString().ToLowerInvariant(); + + invoiceData.Status = status.ToString().ToLowerInvariant(); invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant(); _eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName)); - await context.SaveChangesAsync().ConfigureAwait(false); + await context.SaveChangesAsync(); } return true; } + public async Task GetInvoice(string id, bool inludeAddressData = false) + { + var res = await GetInvoiceRaw(id, inludeAddressData); + return res == null ? null : ToEntity(res); + } + + private async Task GetInvoiceRaw(string id, bool inludeAddressData = false) { using (var context = _ContextFactory.CreateContext()) { @@ -499,7 +508,7 @@ retry: if (invoice == null) return null; - return ToEntity(invoice); + return invoice; } } diff --git a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml index 4919520d1..719f63445 100644 --- a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml @@ -1,4 +1,5 @@ -@model (InvoiceDetailsModel Invoice, bool ShowAddress) +@using BTCPayServer.Client.Models +@model (InvoiceDetailsModel Invoice, bool ShowAddress) @{ var invoice = Model.Invoice; } @inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json new file mode 100644 index 000000000..78179f9ec --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -0,0 +1,712 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/invoices": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Get invoices", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + } + ], + "description": "View information about the existing invoices", + "operationId": "Invoices_GetInvoices", + "responses": { + "200": { + "description": "list of invoices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceDataList" + } + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewinvoices" + ], + "Basic": [] + } + ] + }, + "post": { + "tags": [ + "Invoices" + ], + "summary": "Create a new invoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + } + ], + "description": "Create a new invoice", + "operationId": "Invoices_CreateInvoice", + "responses": { + "200": { + "description": "Information about the new invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when creating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to add new invoices" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInvoiceRequest" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.cancreateinvoice" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Get invoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to fetch", + "schema": { + "type": "string" + } + } + ], + "description": "View information about the specified invoice", + "operationId": "Invoices_GetInvoice", + "responses": { + "200": { + "description": "specified invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the specified invoie" + }, + "404": { + "description": "The key is not found for this invoice" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewinvoices" + ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "Invoices" + ], + "summary": "Archive invoice", + "description": "Archives the specified invoice.", + "operationId": "Invoices_ArchiveInvoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store the invoice belongs to", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to remove", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The invoice has been archived" + }, + "400": { + "description": "A list of errors that occurred when archiving the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to archive the specified invoice" + }, + "404": { + "description": "The key is not found for this invoice" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + }, + "put": { + "tags": [ + "Invoices" + ], + "summary": "Update invoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to update", + "schema": { + "type": "string" + } + } + ], + "description": "Update an invoice", + "operationId": "Invoices_UpdateInvoice", + "responses": { + "200": { + "description": "The updated invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the invoice" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateInvoiceRequest" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "InvoiceDataList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceData" + } + }, + "UpdateInvoiceRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "archived": { + "type": "boolean", + "nullable": true, + "description": "Unarchive an invoice. You can only archive from the HTTP DELETE endpoint." + }, + "status": { + "nullable": true, + "description": "Mark an invoice as completed or invalid.", + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceStatusMark" + } + ] + }, + "email": { + "type": "string", + "nullable": true, + "description": "Sets the customer email, if it was not set before." + } + } + }, + "InvoiceStatusMark": { + "type": "string", + "description": "", + "x-enumNames": [ + "Invalid", + "Complete" + ], + "enum": [ + "Invalid", + "Complete" + ] + }, + "InvoiceStatus": { + "type": "string", + "description": "", + "x-enumNames": [ + "New", + "Paid", + "Expired", + "Invalid", + "Complete", + "Confirmed" + ], + "enum": [ + "New", + "Paid", + "Expired", + "Invalid", + "Complete", + "Confirmed" + ] + }, + "InvoiceExceptionStatus": { + "type": "string", + "description": "", + "x-enumNames": [ + "None", + "PaidLate", + "PaidPartial", + "Marked", + "Invalid", + "PaidOver" + ], + "enum": [ + "None", + "PaidLate", + "PaidPartial", + "Marked", + "Invalid", + "PaidOver" + ] + }, + "InvoiceData": { + "allOf": [ + { + "$ref": "#/components/schemas/CreateInvoiceRequest" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "nullable": true, + "description": "The identifier of the invoice" + }, + "status": { + "$ref": "#/components/schemas/InvoiceStatus", + "description": "The status of the invoice" + }, + "exceptionStatus": { + "$ref": "#/components/schemas/InvoiceExceptionStatus", + "description": "a secondary status of the invoice" + }, + "paymentMethodData": { + "type": "object", + "nullable": false, + "additionalProperties": { + "$ref": "#/components/schemas/PaymentMethodDataModel" + }, + "description": "Activated payment methods details" + } + } + } + ] + }, + "PaymentMethodDataModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "destination": { + "type": "string", + "nullable": true, + "description": "The destination the payment must be made to" + }, + "paymentLink": { + "type": "string", + "nullable": true, + "description": "A payment link that helps pay to the payment destination" + }, + "rate": { + "type": "string", + "format": "decimal", + "description": "The rate between this payment method's currency and the invoice currency" + }, + "paymentMethodPaid": { + "type": "string", + "format": "decimal", + "description": "The amount paid by this payment method" + }, + "totalPaid": { + "type": "string", + "format": "decimal", + "description": "The total amount paid by all payment methods to the invoice, converted to this payment method's currency" + }, + "due": { + "type": "string", + "format": "decimal", + "description": "The total amount left to be paid, converted to this payment method's currency" + }, + "amount": { + "type": "string", + "format": "decimal", + "description": "The invoice amount, converted to this payment method's currency" + }, + "networkFee": { + "type": "string", + "format": "decimal", + "description": "The added merchant fee to pay for network costs of this payment method." + }, + "payments": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/Payment" + }, + "description": "Payments made with this payment method." + } + } + }, + "Payment": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "nullable": true, + "description": "A unique identifier for this payment" + }, + "receivedDate": { + "type": "string", + "format": "date-time", + "description": "The date the payment was recorded" + }, + "value": { + "type": "string", + "format": "decimal", + "description": "The value of the payment" + }, + "fee": { + "type": "string", + "format": "decimal", + "description": "The fee paid for the payment" + }, + "status": { + "$ref": "#/components/schemas/PaymentStatus", + "description": "The status of the payment" + }, + "destination": { + "type": "string", + "nullable": true, + "description": "The destination the payment was made to" + } + } + }, + "PaymentStatus": { + "type": "string", + "description": "", + "x-enumNames": [ + "Invalid", + "AwaitingConfirmation", + "AwaitingCompletion", + "Complete" + ], + "enum": [ + "Invalid", + "AwaitingConfirmation", + "AwaitingCompletion", + "Complete" + ] + }, + "CreateInvoiceRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "amount": { + "type": "string", + "format": "decimal", + "description": "The amount of the invoice" + }, + "currency": { + "type": "string", + "nullable": true, + "description": "The currency the invoice will use" + }, + "metadata": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/ProductInformation" + } + ], + "description": "Additional information around the invoice" + }, + "customer": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/BuyerInformation" + } + ], + "description": "Additional information around the entity the invoice is addressed to" + }, + "checkout": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/CheckoutOptions" + } + ], + "description": "Additional settings to customize the checkout flow" + } + } + }, + "ProductInformation": { + "type": "object", + "additionalProperties": false, + "properties": { + "orderId": { + "type": "string", + "nullable": true, + "description": "Can be used by the merchant to assign their own internal ID to an invoice." + }, + "posData": { + "type": "string", + "nullable": true, + "description": "A passthru variable provided by the merchant and designed to be used by the merchant to correlate the invoice with an order or other object in their system. This passthru variable can be a serialized object, e.g.:" + }, + "itemDesc": { + "type": "string", + "nullable": true, + "description": "Invoice description - will be added as a line item on the checkout page, under the merchant name" + }, + "itemCode": { + "type": "string", + "nullable": true, + "description": "An item code, for a specific product code that is being paid for. Usually used in conjunction with the POS/Crowdfund apps" + }, + "physical": { + "type": "boolean", + "description": "Whether it was a physical purchase" + }, + "taxIncluded": { + "type": "string", + "format": "decimal", + "nullable": true, + "description": "How much tax is included in the invoice amount" + } + } + }, + "BuyerInformation": { + "type": "object", + "additionalProperties": false, + "properties": { + "buyerName": { + "type": "string", + "nullable": true + }, + "buyerEmail": { + "type": "string", + "nullable": true + }, + "buyerCountry": { + "type": "string", + "nullable": true + }, + "buyerZip": { + "type": "string", + "nullable": true + }, + "buyerState": { + "type": "string", + "nullable": true + }, + "buyerCity": { + "type": "string", + "nullable": true + }, + "buyerAddress2": { + "type": "string", + "nullable": true + }, + "buyerAddress1": { + "type": "string", + "nullable": true + }, + "buyerPhone": { + "type": "string", + "nullable": true + } + } + }, + "CheckoutOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "speedPolicy": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/SpeedPolicy" + } + ], + "description": "This is a risk mitigation parameter for the merchant to configure how they want to fulfill orders depending on the number of block confirmations for the transaction made by the consumer on the selected cryptocurrency" + }, + "paymentMethods": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + }, + "description": "A specific set of payment methods to use for this invoice" + }, + "redirectAutomatically": { + "type": "boolean", + "nullable": true, + "description": "Whether to redirect to the redirectUri automatically after the invoice has been paid. Default to the store setting (which is `false` by default)" + }, + "redirectUri": { + "type": "string", + "nullable": true, + "description": "This is the URL for a return link that is displayed on the receipt, to return the shopper back to your website after a successful purchase. This could be a page specific to the order, or to their account." + }, + "webHook": { + "type": "string", + "format": "uri", + "nullable": true, + "description":"A URL to send webhook notification to. Sent when the status changes or a payment is detected" + }, + "expirationTime": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the invoice expires. By default will use the store settings (which is set to 15 minutes by default)" + }, + "paymentTolerance": { + "type": "string", + "format": "double", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "A percentage dtermining whether to count the invoice as paid when the invoice is paid within the specified margin of error" + } + } + }, + "SpeedPolicy": { + "type": "string", + "description": "", + "x-enumNames": [ + "HighSpeed", + "MediumSpeed", + "LowSpeed", + "LowMediumSpeed" + ], + "enum": [ + "HighSpeed", + "MediumSpeed", + "LowSpeed", + "LowMediumSpeed" + ] + } + } + }, + "tags": [ + { + "name": "Invoices" + } + ] +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json index 55c936206..9bbde2c4b 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json @@ -122,7 +122,7 @@ } ], "description": "View information about the specified payment request", - "operationId": "PaymentRequests_GetPaymentRequests", + "operationId": "PaymentRequests_GetPaymentRequest", "responses": { "200": { "description": "specified payment request", @@ -218,7 +218,7 @@ "name": "paymentRequestId", "in": "path", "required": true, - "description": "The payment request to remove", + "description": "The payment request to update", "schema": { "type": "string" } } ], From 5f6f54db36bae44e6e7ac9197e0303780a329220 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 27 Jul 2020 09:46:23 +0200 Subject: [PATCH 08/16] Remove old props --- .../Models/CreateInvoiceRequest.cs | 52 +------------ .../GreenField/InvoiceController.cs | 75 ++++++++----------- .../swagger/v1/swagger.template.invoices.json | 23 ++---- 3 files changed, 43 insertions(+), 107 deletions(-) diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 3987ff1c9..23fba6566 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -9,13 +9,9 @@ namespace BTCPayServer.Client.Models { [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } - public string Currency { get; set; } - - public ProductInformation Metadata { get; set; } - - public BuyerInformation Customer { get; set; } = new BuyerInformation(); - + public string Metadata { get; set; } + public string CustomerEmail { get; set; } public CheckoutOptions Checkout { get; set; } = new CheckoutOptions(); public class CheckoutOptions @@ -34,49 +30,5 @@ namespace BTCPayServer.Client.Models [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] 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/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index e0dd86743..2a3cd87aa 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -13,6 +13,8 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitpayClient; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; @@ -121,10 +123,10 @@ namespace BTCPayServer.Controllers.GreenField } } - if (!string.IsNullOrEmpty(request.Customer.BuyerEmail) && - !EmailValidator.IsEmail(request.Customer.BuyerEmail)) + if (!string.IsNullOrEmpty(request.CustomerEmail) && + !EmailValidator.IsEmail(request.CustomerEmail)) { - request.AddModelError(invoiceRequest => invoiceRequest.Customer.BuyerEmail, "Invalid email address", + request.AddModelError(invoiceRequest => invoiceRequest.CustomerEmail, "Invalid email address", this); } @@ -226,27 +228,8 @@ namespace BTCPayServer.Controllers.GreenField Status = entity.Status, ExceptionStatus = entity.ExceptionStatus, 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 - }, + Metadata = entity.PosData, + CustomerEmail = entity.RefundMail ?? entity.BuyerInformation.BuyerEmail, Checkout = new CreateInvoiceRequest.CheckoutOptions() { ExpirationTime = entity.ExpirationTime, @@ -306,37 +289,45 @@ namespace BTCPayServer.Controllers.GreenField private Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity) { + Buyer buyer = null; + ProductInformation pi = null; + JToken? orderId = null; + if (!string.IsNullOrEmpty(entity.Metadata) && entity.Metadata.StartsWith('{')) + { + //metadata was provided and is json. Let's try and match props + try + { + buyer = JsonConvert.DeserializeObject(entity.Metadata); + pi = JsonConvert.DeserializeObject(entity.Metadata); + JObject.Parse(entity.Metadata).TryGetValue("orderid", StringComparison.InvariantCultureIgnoreCase, + out orderId); + } + catch + { + // ignored + } + } 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, - }, + Buyer = buyer, + BuyerEmail = entity.CustomerEmail, 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 + PosData = entity.Metadata, + Physical = pi?.Physical??false, + ItemCode = pi?.ItemCode, + ItemDesc = pi?.ItemDesc, + TaxIncluded = pi?.TaxIncluded, + OrderId = orderId?.ToString() }; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 78179f9ec..26d5ce0d1 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -521,28 +521,21 @@ "format": "decimal", "description": "The amount of the invoice" }, + "customerEmail": { + "type": "string", + "format": "email", + "nullable": true, + "description": "The email of the customer. If the store is configured to ask for a refund email, the checkout UI will prompt for one when not provided." + }, "currency": { "type": "string", "nullable": true, "description": "The currency the invoice will use" }, "metadata": { + "type": "string", "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/ProductInformation" - } - ], - "description": "Additional information around the invoice" - }, - "customer": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/BuyerInformation" - } - ], - "description": "Additional information around the entity the invoice is addressed to" + "description": "Additional information around the invoice that can be supplied.
You can pass a json body with values of `ProductInformation` `BuyerInformation` which will populate the fields to stay compatible with previous workflows (and bitpay invoice api)./>" }, "checkout": { "nullable": true, From 8dea7df82af0b63980d9e3fc2799999b7d0f78e8 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 27 Jul 2020 10:43:35 +0200 Subject: [PATCH 09/16] make individual action items --- .../BTCPayServerClient.Invoices.cs | 29 ++- .../Models/AddCustomerEmailRequest.cs | 7 + BTCPayServer.Client/Models/InvoiceData.cs | 2 +- .../Models/MarkInvoiceStatusRequest.cs | 11 ++ .../Models/UpdateInvoiceRequest.cs | 9 - BTCPayServer.Tests/GreenfieldAPITests.cs | 26 +-- .../GreenField/InvoiceController.cs | 118 ++++++++---- .../GreenField/PaymentRequestsController.cs | 4 +- .../swagger/v1/swagger.template.invoices.json | 177 ++++++++++++++++-- 9 files changed, 296 insertions(+), 87 deletions(-) create mode 100644 BTCPayServer.Client/Models/AddCustomerEmailRequest.cs create mode 100644 BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs delete mode 100644 BTCPayServer.Client/Models/UpdateInvoiceRequest.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index aae9ddea7..d97f235db 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -47,14 +47,35 @@ namespace BTCPayServer.Client return await HandleResponse(response); } - public virtual async Task UpdateInvoice(string storeId, string invoiceId, - UpdateInvoiceRequest request, CancellationToken token = default) + public virtual async Task AddCustomerEmailToInvoice(string storeId, string invoiceId, + AddCustomerEmailRequest request, CancellationToken token = default) { if (request == null) throw new ArgumentNullException(nameof(request)); var response = await _httpClient.SendAsync( - CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}", bodyPayload: request, - method: HttpMethod.Put), token); + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/email", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public virtual async Task MarkInvoiceStatus(string storeId, string invoiceId, + MarkInvoiceStatusRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (request.Status!= InvoiceStatus.Complete && request.Status!= InvoiceStatus.Invalid) + throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete"); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public virtual async Task UnarchiveInvoice(string storeId, string invoiceId, CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive", + method: HttpMethod.Post), token); return await HandleResponse(response); } } diff --git a/BTCPayServer.Client/Models/AddCustomerEmailRequest.cs b/BTCPayServer.Client/Models/AddCustomerEmailRequest.cs new file mode 100644 index 000000000..f34e15c86 --- /dev/null +++ b/BTCPayServer.Client/Models/AddCustomerEmailRequest.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Client.Models +{ + public class AddCustomerEmailRequest + { + public string Email { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 83cb597e4..cb30793f6 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -12,7 +12,7 @@ namespace BTCPayServer.Client.Models [JsonConverter(typeof(StringEnumConverter))] public InvoiceStatus Status { get; set; } [JsonConverter(typeof(StringEnumConverter))] - public InvoiceExceptionStatus ExceptionStatus { get; set; } + public InvoiceExceptionStatus AdditionalStatus { get; set; } public Dictionary PaymentMethodData { get; set; } public class PaymentMethodDataModel diff --git a/BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs b/BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs new file mode 100644 index 000000000..a06919dad --- /dev/null +++ b/BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class MarkInvoiceStatusRequest + { + [JsonConverter(typeof(StringEnumConverter))] + public InvoiceStatus Status { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs b/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs deleted file mode 100644 index 5b3ebf983..000000000 --- a/BTCPayServer.Client/Models/UpdateInvoiceRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BTCPayServer.Client.Models -{ - public class UpdateInvoiceRequest - { - public bool? Archived { get; set; } - public InvoiceStatus? Status { get; set; } - public string Email { get; set; } - } -} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 7c7f57ebf..54511cf90 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -762,7 +762,7 @@ namespace BTCPayServer.Tests }); await user.RegisterDerivationSchemeAsync("BTC"); var newInvoice = await client.CreateInvoice(user.StoreId, - new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = new CreateInvoiceRequest.ProductInformation(){ ItemCode = "testitem"}}); + new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = "{\"itemCode\": \"testitem\"}"}); //list var invoices = await viewOnly.GetInvoices(user.StoreId); @@ -773,29 +773,34 @@ namespace BTCPayServer.Tests //get payment request var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); - Assert.Equal(newInvoice.Metadata.ItemCode, invoice.Metadata.ItemCode); + Assert.Equal(newInvoice.Metadata, invoice.Metadata); //update await AssertHttpError(403, async () => { - await viewOnly.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + await viewOnly.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() { Email = "j@g.com" }); }); - await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() { Email = "j@g.com" }); invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); - Assert.Equal(invoice.Customer.BuyerEmail, "j@g.com"); + Assert.Equal("j@g.com", invoice.CustomerEmail); - await AssertValidationError(new[] { nameof(UpdateInvoiceRequest.Email), nameof(UpdateInvoiceRequest.Archived),nameof(UpdateInvoiceRequest.Status) }, async () => + await AssertValidationError(new[] { nameof(AddCustomerEmailRequest.Email) }, async () => { - await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() + await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() { Email = "j@g2.com", - Archived = true, + }); + }); + await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () => + { + await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() + { Status = InvoiceStatus.Complete }); }); @@ -812,10 +817,7 @@ namespace BTCPayServer.Tests (await client.GetInvoices(user.StoreId)).Select(data => data.Id)); //unarchive - await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() - { - Archived = false, - }); + await client.UnarchiveInvoice(user.StoreId, invoice.Id); Assert.NotNull(await client.GetInvoice(user.StoreId,invoice.Id)); } diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 2a3cd87aa..1ed32983f 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -154,14 +154,15 @@ namespace BTCPayServer.Controllers.GreenField } catch (BitpayHttpException e) { - return this.CreateAPIError(null, e.Message); + return this.CreateAPIError(null, e.Message); } } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")] - public async Task UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request) + [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")] + public async Task MarkInvoiceStatus(string storeId, string invoiceId, + MarkInvoiceStatusRequest request) { var store = HttpContext.GetStoreData(); if (store == null) @@ -175,42 +176,10 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - if (request.Archived.HasValue) + if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status)) { - if (request.Archived.Value && !invoice.Archived) - { - ModelState.AddModelError(nameof(request.Archived), - "You can only archive an invoice via HTTP DELETE."); - } - else if (!request.Archived.Value && invoice.Archived) - { - await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId); - } - } - - if (request.Status != null) - { - if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status.Value)) - { - ModelState.AddModelError(nameof(request.Status), - "Status can only be marked to invalid or complete within certain conditions."); - } - } - - if (request.Email != null) - { - if (!EmailValidator.IsEmail(request.Email)) - { - request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address", - this); - } - else if (!string.IsNullOrEmpty(invoice.BuyerInformation.BuyerEmail)) - { - request.AddModelError(invoiceRequest => invoiceRequest.Email, "Email address already set", - this); - } - - await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() {Email = request.Email}); + ModelState.AddModelError(nameof(request.Status), + "Status can only be marked to invalid or complete within certain conditions."); } if (!ModelState.IsValid) @@ -219,6 +188,74 @@ namespace BTCPayServer.Controllers.GreenField return await GetInvoice(storeId, invoiceId); } + [Authorize(Policy = Policies.CanCreateInvoice, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/email")] + public async Task AddCustomerEmail(string storeId, string invoiceId, + AddCustomerEmailRequest request) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice.StoreId != store.Id) + { + return NotFound(); + } + + if (!EmailValidator.IsEmail(request.Email)) + { + request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address", + this); + } + else if (!string.IsNullOrEmpty(invoice.BuyerInformation.BuyerEmail)) + { + request.AddModelError(invoiceRequest => invoiceRequest.Email, "Email address already set", + this); + } + + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + + await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() {Email = request.Email}); + + return await GetInvoice(storeId, invoiceId); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")] + public async Task UnarchiveInvoice(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(); + } + + 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); + } + + private InvoiceData ToModel(InvoiceEntity entity) { return new InvoiceData() @@ -226,7 +263,7 @@ namespace BTCPayServer.Controllers.GreenField Amount = entity.ProductInformation.Price, Id = entity.Id, Status = entity.Status, - ExceptionStatus = entity.ExceptionStatus, + AdditionalStatus = entity.ExceptionStatus, Currency = entity.ProductInformation.Currency, Metadata = entity.PosData, CustomerEmail = entity.RefundMail ?? entity.BuyerInformation.BuyerEmail, @@ -307,6 +344,7 @@ namespace BTCPayServer.Controllers.GreenField // ignored } } + return new Models.CreateInvoiceRequest() { Buyer = buyer, @@ -323,7 +361,7 @@ namespace BTCPayServer.Controllers.GreenField PaymentCurrencies = entity.Checkout.PaymentMethods, NotificationURL = entity.Checkout.RedirectUri, PosData = entity.Metadata, - Physical = pi?.Physical??false, + Physical = pi?.Physical ?? false, ItemCode = pi?.ItemCode, ItemDesc = pi?.ItemDesc, TaxIncluded = pi?.TaxIncluded, diff --git a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs index 4f25999dc..583c5f978 100644 --- a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs +++ b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs @@ -32,10 +32,10 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/payment-requests")] - public async Task>> GetPaymentRequests(string storeId) + public async Task>> GetPaymentRequests(string storeId, bool includeArchived = false) { var prs = await _paymentRequestRepository.FindPaymentRequests( - new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = false }); + new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived }); return Ok(prs.Items.Select(FromModel)); } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 26d5ce0d1..c3dc3c6c7 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -214,12 +214,14 @@ "Basic": [] } ] - }, - "put": { + } + }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}/email": { + "post": { "tags": [ "Invoices" ], - "summary": "Update invoice", + "summary": "Add customer email to invoice", "parameters": [ { "name": "storeId", @@ -240,8 +242,8 @@ } } ], - "description": "Update an invoice", - "operationId": "Invoices_UpdateInvoice", + "description": "Adds the customer's email to the invoice if it has not been set already.", + "operationId": "Invoices_AddCustomerEmail", "responses": { "200": { "description": "The updated invoice", @@ -272,11 +274,147 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateInvoiceRequest" + "$ref": "#/components/schemas/AddCustomerEmailRequest" } } } }, + "security": [ + { + "API Key": [ + "btcpay.store.cancreateinvoice" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}/status": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Mark invoice status", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to update", + "schema": { + "type": "string" + } + } + ], + "description": "Mark an invoice as invalid or completed.", + "operationId": "Invoices_MarkInvoiceStatus", + "responses": { + "200": { + "description": "The updated invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the invoice" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkInvoiceStatusRequest" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Unarchive invoice", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to update", + "schema": { + "type": "string" + } + } + ], + "description": "Unarchive an invoice", + "operationId": "Invoices_UnarchiveInvoice", + "responses": { + "200": { + "description": "The unarchived invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the invoice" + } + }, "security": [ { "API Key": [ @@ -296,27 +434,28 @@ "$ref": "#/components/schemas/InvoiceData" } }, - "UpdateInvoiceRequest": { + "MarkInvoiceStatusRequest": { "type": "object", "additionalProperties": false, "properties": { - "archived": { - "type": "boolean", - "nullable": true, - "description": "Unarchive an invoice. You can only archive from the HTTP DELETE endpoint." - }, "status": { - "nullable": true, + "nullable": false, "description": "Mark an invoice as completed or invalid.", "oneOf": [ { "$ref": "#/components/schemas/InvoiceStatusMark" } ] - }, + } + } + }, + "AddCustomerEmailRequest": { + "type": "object", + "additionalProperties": false, + "properties": { "email": { "type": "string", - "nullable": true, + "nullable": false, "description": "Sets the customer email, if it was not set before." } } @@ -353,9 +492,9 @@ "Confirmed" ] }, - "InvoiceExceptionStatus": { + "InvoiceAdditionalStatus": { "type": "string", - "description": "", + "description": "An additional status that describes why an invoice is in its current status.", "x-enumNames": [ "None", "PaidLate", @@ -391,8 +530,8 @@ "$ref": "#/components/schemas/InvoiceStatus", "description": "The status of the invoice" }, - "exceptionStatus": { - "$ref": "#/components/schemas/InvoiceExceptionStatus", + "additionalStatus": { + "$ref": "#/components/schemas/InvoiceAdditionalStatus", "description": "a secondary status of the invoice" }, "paymentMethodData": { From b2ff041ec0dff2dc77fc56c9dbd71a9322c7f6c1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 25 Aug 2020 14:33:00 +0900 Subject: [PATCH 10/16] Create Metadata property for InvoiceEntity, migrate all data without logic there --- .../Models/CreateInvoiceRequest.cs | 4 +- BTCPayServer.Tests/BTCPayServer.Tests.csproj | 3 - BTCPayServer.Tests/GreenfieldAPITests.cs | 112 ++++++-- BTCPayServer.Tests/UnitTest1.cs | 11 +- .../Controllers/AppsPublicController.cs | 4 +- .../GreenField/InvoiceController.cs | 73 +++-- .../Controllers/InvoiceController.API.cs | 2 +- .../Controllers/InvoiceController.UI.cs | 47 ++-- BTCPayServer/Controllers/InvoiceController.cs | 78 +++--- .../Controllers/PaymentRequestController.cs | 4 +- BTCPayServer/Controllers/PublicController.cs | 2 +- BTCPayServer/Data/InvoiceDataExtensions.cs | 12 + .../AppInventoryUpdaterHostedService.cs | 9 +- ...quest.cs => BitpayCreateInvoiceRequest.cs} | 4 +- .../InvoicingModels/InvoiceDetailsModel.cs | 18 +- .../PaymentRequest/PaymentRequestService.cs | 6 +- .../Lightning/LightningLikePaymentHandler.cs | 6 +- BTCPayServer/Services/Apps/AppService.cs | 10 +- .../Services/Invoices/Export/InvoiceExport.cs | 17 +- .../Services/Invoices/InvoiceEntity.cs | 252 ++++++++++++++---- .../Services/Invoices/InvoiceRepository.cs | 18 +- BTCPayServer/Views/Invoice/Invoice.cshtml | 30 +-- 22 files changed, 463 insertions(+), 259 deletions(-) rename BTCPayServer/Models/{CreateInvoiceRequest.cs => BitpayCreateInvoiceRequest.cs} (97%) diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 23fba6566..621993687 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -2,6 +2,7 @@ using System; using BTCPayServer.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Client.Models { @@ -10,8 +11,7 @@ namespace BTCPayServer.Client.Models [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } public string Currency { get; set; } - public string Metadata { get; set; } - public string CustomerEmail { get; set; } + public JObject Metadata { get; set; } public CheckoutOptions Checkout { get; set; } = new CheckoutOptions(); public class CheckoutOptions diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 76ffc3d2d..f7d08c634 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -17,9 +17,6 @@ $(DefineConstants);SHORT_TIMEOUT - - $(DefineConstants);ALTCOINS - diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 54511cf90..c17d83fe8 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -10,6 +10,7 @@ using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.JsonConverters; using BTCPayServer.Services; +using BTCPayServer.Services.Invoices; using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -17,6 +18,7 @@ using NBitcoin; using NBitpayClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NUglify.Helpers; using Xunit; using Xunit.Abstractions; using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; @@ -731,8 +733,75 @@ namespace BTCPayServer.Tests }); } } - - + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task InvoiceLegacyTests() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + var client = await user.CreateClient(Policies.Unrestricted); + var oldBitpay = user.BitPay; + + Logs.Tester.LogInformation("Let's create an invoice with bitpay API"); + var oldInvoice = await oldBitpay.CreateInvoiceAsync(new Invoice() + { + Currency = "BTC", + Price = 1000.19392922m, + BuyerAddress1 = "blah", + Buyer = new Buyer() + { + Address2 = "blah2" + }, + ItemCode = "code", + ItemDesc = "desc", + OrderId = "orderId", + PosData = "posData" + }); + + async Task AssertInvoiceMetadata() + { + Logs.Tester.LogInformation("Let's check if we can get invoice in the new format with the metadata"); + var newInvoice = await client.GetInvoice(user.StoreId, oldInvoice.Id); + Assert.Equal("posData", newInvoice.Metadata["posData"].Value()); + Assert.Equal("code", newInvoice.Metadata["itemCode"].Value()); + Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value()); + Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value()); + Assert.False(newInvoice.Metadata["physical"].Value()); + Assert.Null(newInvoice.Metadata["buyerCountry"]); + Assert.Equal(1000.19392922m, newInvoice.Amount); + Assert.Equal("BTC", newInvoice.Currency); + return newInvoice; + } + + await AssertInvoiceMetadata(); + + Logs.Tester.LogInformation("Let's hack the Bitpay created invoice to be just like before this update. (Invoice V1)"); + var invoiceV1 = "{\r\n \"version\": 1,\r\n \"id\": \"" + oldInvoice.Id + "\",\r\n \"storeId\": \"" + user.StoreId + "\",\r\n \"orderId\": \"orderId\",\r\n \"speedPolicy\": 1,\r\n \"rate\": 1.0,\r\n \"invoiceTime\": 1598329634,\r\n \"expirationTime\": 1598330534,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\",\r\n \"productInformation\": {\r\n \"itemDesc\": \"desc\",\r\n \"itemCode\": \"code\",\r\n \"physical\": false,\r\n \"price\": 1000.19392922,\r\n \"currency\": \"BTC\"\r\n },\r\n \"buyerInformation\": {\r\n \"buyerName\": null,\r\n \"buyerEmail\": null,\r\n \"buyerCountry\": null,\r\n \"buyerZip\": null,\r\n \"buyerState\": null,\r\n \"buyerCity\": null,\r\n \"buyerAddress2\": \"blah2\",\r\n \"buyerAddress1\": \"blah\",\r\n \"buyerPhone\": null\r\n },\r\n \"posData\": \"posData\",\r\n \"internalTags\": [],\r\n \"derivationStrategy\": null,\r\n \"derivationStrategies\": \"{\\\"BTC\\\":{\\\"signingKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\",\\\"source\\\":\\\"NBXplorer\\\",\\\"accountDerivation\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf-[legacy]\\\",\\\"accountOriginal\\\":null,\\\"accountKeySettings\\\":[{\\\"rootFingerprint\\\":\\\"54d5044d\\\",\\\"accountKeyPath\\\":\\\"44'/1'/0'\\\",\\\"accountKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\"}],\\\"label\\\":null}}\",\r\n \"status\": \"new\",\r\n \"exceptionStatus\": \"\",\r\n \"payments\": [],\r\n \"refundable\": false,\r\n \"refundMail\": null,\r\n \"redirectURL\": null,\r\n \"redirectAutomatically\": false,\r\n \"txFee\": 0,\r\n \"fullNotifications\": false,\r\n \"notificationEmail\": null,\r\n \"notificationURL\": null,\r\n \"serverUrl\": \"http://127.0.0.1:8001\",\r\n \"cryptoData\": {\r\n \"BTC\": {\r\n \"rate\": 1.0,\r\n \"paymentMethod\": {\r\n \"networkFeeMode\": 0,\r\n \"networkFeeRate\": 100.0,\r\n \"payjoinEnabled\": false\r\n },\r\n \"feeRate\": 100.0,\r\n \"txFee\": 0,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\"\r\n }\r\n },\r\n \"monitoringExpiration\": 1598416934,\r\n \"historicalAddresses\": null,\r\n \"availableAddressHashes\": null,\r\n \"extendedNotifications\": false,\r\n \"events\": null,\r\n \"paymentTolerance\": 0.0,\r\n \"archived\": false\r\n}"; + var db = tester.PayTester.GetService(); + using var ctx = db.CreateContext(); + var dbInvoice = await ctx.Invoices.FindAsync(oldInvoice.Id); + dbInvoice.Blob = ZipUtils.Zip(invoiceV1); + await ctx.SaveChangesAsync(); + var newInvoice = await AssertInvoiceMetadata(); + + Logs.Tester.LogInformation("Now, let's create an invoice with the new API but with the same metadata as Bitpay"); + newInvoice.Metadata.Add("lol", "lol"); + newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() + { + Metadata = newInvoice.Metadata, + Amount = 1000.19392922m, + Currency = "BTC" + }); + oldInvoice = await oldBitpay.GetInvoiceAsync(newInvoice.Id); + await AssertInvoiceMetadata(); + Assert.Equal("lol", newInvoice.Metadata["lol"].Value()); + } + } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] @@ -750,11 +819,11 @@ namespace BTCPayServer.Tests //create //validation errors - await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () => - { - await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions(){ PaymentTolerance = -2, PaymentMethods = new []{"jasaas_sdsad"}}}); - }); - + await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () => + { + await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } }); + }); + await AssertHttpError(403, async () => { await viewOnly.CreateInvoice(user.StoreId, @@ -762,7 +831,7 @@ namespace BTCPayServer.Tests }); await user.RegisterDerivationSchemeAsync("BTC"); var newInvoice = await client.CreateInvoice(user.StoreId, - new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = "{\"itemCode\": \"testitem\"}"}); + new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") }); //list var invoices = await viewOnly.GetInvoices(user.StoreId); @@ -788,15 +857,7 @@ namespace BTCPayServer.Tests Email = "j@g.com" }); invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); - Assert.Equal("j@g.com", invoice.CustomerEmail); - await AssertValidationError(new[] { nameof(AddCustomerEmailRequest.Email) }, async () => - { - await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() - { - Email = "j@g2.com", - }); - }); await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () => { await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() @@ -804,8 +865,8 @@ namespace BTCPayServer.Tests Status = InvoiceStatus.Complete }); }); - - + + //archive await AssertHttpError(403, async () => { @@ -815,15 +876,15 @@ namespace BTCPayServer.Tests await client.ArchiveInvoice(user.StoreId, invoice.Id); Assert.DoesNotContain(invoice.Id, (await client.GetInvoices(user.StoreId)).Select(data => data.Id)); - + //unarchive await client.UnarchiveInvoice(user.StoreId, invoice.Id); - Assert.NotNull(await client.GetInvoice(user.StoreId,invoice.Id)); - + Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id)); + } } - + [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] @@ -855,10 +916,11 @@ namespace BTCPayServer.Tests Assert.Throws(() => { jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null); - });Assert.Throws(() => - { - jsonConverter.ReadJson(Get("null"), typeof(double), null, null); }); + Assert.Throws(() => + { + jsonConverter.ReadJson(Get("null"), typeof(double), null, null); + }); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null)); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null)); Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null)); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5b5930b98..6b807c6ae 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -53,7 +53,6 @@ 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 @@ -326,7 +325,7 @@ namespace BTCPayServer.Tests Assert.True(Torrc.TryParse(input, out torrc)); Assert.Equal(expected, torrc.ToString()); } - +#if ALTCOINS [Fact] [Trait("Fast", "Fast")] public void CanCalculateCryptoDue() @@ -347,7 +346,7 @@ namespace BTCPayServer.Tests Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.Price = 5000; var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); @@ -397,7 +396,7 @@ namespace BTCPayServer.Tests entity = new InvoiceEntity(); entity.Networks = networkProvider; - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.Price = 5000; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); paymentMethods.Add( new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); @@ -491,7 +490,7 @@ namespace BTCPayServer.Tests Assert.Equal(accounting.Paid, accounting.TotalDue); #pragma warning restore CS0618 } - +#endif [Fact] [Trait("Integration", "Integration")] public async Task CanUseTestWebsiteUI() @@ -546,7 +545,7 @@ namespace BTCPayServer.Tests Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); - entity.ProductInformation = new ProductInformation() { Price = 5000 }; + entity.Price = 5000; entity.PaymentTolerance = 0; diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 7da71f701..901044fd7 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -175,7 +175,7 @@ namespace BTCPayServer.Controllers var store = await _AppService.GetStore(app); try { - var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() + var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest() { ItemCode = choice?.Id, ItemDesc = title, @@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers try { - var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() + var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest() { OrderId = AppService.GetCrowdfundOrderId(appId), Currency = settings.TargetCurrency, diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 1ed32983f..05aebc392 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -11,6 +11,7 @@ using BTCPayServer.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; using NBitcoin; using NBitpayClient; using Newtonsoft.Json; @@ -48,7 +49,8 @@ namespace BTCPayServer.Controllers.GreenField var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() { - StoreId = new[] {store.Id}, IncludeArchived = includeArchived + StoreId = new[] { store.Id }, + IncludeArchived = includeArchived }); return Ok(invoices.Select(ToModel)); @@ -123,13 +125,6 @@ namespace BTCPayServer.Controllers.GreenField } } - if (!string.IsNullOrEmpty(request.CustomerEmail) && - !EmailValidator.IsEmail(request.CustomerEmail)) - { - request.AddModelError(invoiceRequest => invoiceRequest.CustomerEmail, "Invalid email address", - this); - } - if (request.Checkout.ExpirationTime != null && request.Checkout.ExpirationTime < DateTime.Now) { request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExpirationTime, @@ -211,7 +206,7 @@ namespace BTCPayServer.Controllers.GreenField request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address", this); } - else if (!string.IsNullOrEmpty(invoice.BuyerInformation.BuyerEmail)) + else if (!string.IsNullOrEmpty(invoice.Metadata.BuyerEmail)) { request.AddModelError(invoiceRequest => invoiceRequest.Email, "Email address already set", this); @@ -220,7 +215,7 @@ namespace BTCPayServer.Controllers.GreenField if (!ModelState.IsValid) return this.CreateValidationError(ModelState); - await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() {Email = request.Email}); + await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() { Email = request.Email }); return await GetInvoice(storeId, invoiceId); } @@ -260,13 +255,12 @@ namespace BTCPayServer.Controllers.GreenField { return new InvoiceData() { - Amount = entity.ProductInformation.Price, + Amount = entity.Price, Id = entity.Id, Status = entity.Status, AdditionalStatus = entity.ExceptionStatus, - Currency = entity.ProductInformation.Currency, - Metadata = entity.PosData, - CustomerEmail = entity.RefundMail ?? entity.BuyerInformation.BuyerEmail, + Currency = entity.Currency, + Metadata = entity.Metadata.ToJObject(), Checkout = new CreateInvoiceRequest.CheckoutOptions() { ExpirationTime = entity.ExpirationTime, @@ -324,31 +318,27 @@ namespace BTCPayServer.Controllers.GreenField }; } - private Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity) + private Models.BitpayCreateInvoiceRequest FromModel(CreateInvoiceRequest entity) { - Buyer buyer = null; - ProductInformation pi = null; - JToken? orderId = null; - if (!string.IsNullOrEmpty(entity.Metadata) && entity.Metadata.StartsWith('{')) + InvoiceMetadata invoiceMetadata = null; + if (entity.Metadata != null) { - //metadata was provided and is json. Let's try and match props - try - { - buyer = JsonConvert.DeserializeObject(entity.Metadata); - pi = JsonConvert.DeserializeObject(entity.Metadata); - JObject.Parse(entity.Metadata).TryGetValue("orderid", StringComparison.InvariantCultureIgnoreCase, - out orderId); - } - catch - { - // ignored - } + invoiceMetadata = entity.Metadata.ToObject(); } - - return new Models.CreateInvoiceRequest() + return new Models.BitpayCreateInvoiceRequest() { - Buyer = buyer, - BuyerEmail = entity.CustomerEmail, + Buyer = invoiceMetadata == null ? null : new Buyer() + { + Address1 = invoiceMetadata.BuyerAddress1, + Address2 = invoiceMetadata.BuyerAddress2, + City = invoiceMetadata.BuyerCity, + country = invoiceMetadata.BuyerCountry, + email = invoiceMetadata.BuyerEmail, + Name = invoiceMetadata.BuyerName, + phone = invoiceMetadata.BuyerPhone, + State = invoiceMetadata.BuyerState, + zip = invoiceMetadata.BuyerZip, + }, Currency = entity.Currency, Price = entity.Amount, Refundable = true, @@ -360,12 +350,13 @@ namespace BTCPayServer.Controllers.GreenField TransactionSpeed = entity.Checkout.SpeedPolicy?.ToString(), PaymentCurrencies = entity.Checkout.PaymentMethods, NotificationURL = entity.Checkout.RedirectUri, - PosData = entity.Metadata, - Physical = pi?.Physical ?? false, - ItemCode = pi?.ItemCode, - ItemDesc = pi?.ItemDesc, - TaxIncluded = pi?.TaxIncluded, - OrderId = orderId?.ToString() + PosData = invoiceMetadata?.PosData, + Physical = invoiceMetadata?.Physical ?? false, + ItemCode = invoiceMetadata?.ItemCode, + ItemDesc = invoiceMetadata?.ItemDesc, + TaxIncluded = invoiceMetadata?.TaxIncluded, + OrderId = invoiceMetadata?.OrderId, + Metadata = entity.Metadata }; } } diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 52beaa07f..fca1ded2b 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -29,7 +29,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("invoices")] [MediaTypeConstraint("application/json")] - public async Task> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken) + public async Task> CreateInvoice([FromBody] BitpayCreateInvoiceRequest invoice, CancellationToken cancellationToken) { if (invoice == null) throw new BitpayHttpException(400, "Invalid invoice"); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 9fcb90689..8a10a1e92 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -30,8 +30,7 @@ using NBitcoin; using NBitpayClient; using NBXplorer; using Newtonsoft.Json.Linq; -using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; -using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation; +using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers @@ -54,7 +53,6 @@ namespace BTCPayServer.Controllers if (invoice == null) return NotFound(); - var prodInfo = invoice.ProductInformation; var store = await _StoreRepository.FindStore(invoice.StoreId); var model = new InvoiceDetailsModel() { @@ -70,16 +68,14 @@ namespace BTCPayServer.Controllers CreatedDate = invoice.InvoiceTime, ExpirationDate = invoice.ExpirationTime, MonitoringDate = invoice.MonitoringExpiration, - OrderId = invoice.OrderId, - BuyerInformation = invoice.BuyerInformation, - Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency), - TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency), + Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency), + TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency), NotificationUrl = invoice.NotificationURL?.AbsoluteUri, RedirectUrl = invoice.RedirectURL?.AbsoluteUri, - ProductInformation = invoice.ProductInformation, + TypedMetadata = invoice.Metadata, StatusException = invoice.ExceptionStatus, Events = invoice.Events, - PosData = PosDataParser.ParsePosData(invoice.PosData), + PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData), Archived = invoice.Archived, CanRefund = CanRefund(invoice.GetInvoiceState()), }; @@ -179,7 +175,7 @@ namespace BTCPayServer.Controllers if (!CanRefund(invoice.GetInvoiceState())) return NotFound(); var paymentMethodId = new PaymentMethodId(model.SelectedPaymentMethod, PaymentTypes.BTCLike); - var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.ProductInformation.Currency, true); + var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true); var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8; if (model.SelectedRefundOption is null) { @@ -189,7 +185,7 @@ namespace BTCPayServer.Controllers model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility); model.RateThenText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode, true); var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); - var rateResult = await _RateProvider.FetchRate(new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.ProductInformation.Currency), rules, cancellationToken); + var rateResult = await _RateProvider.FetchRate(new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules, cancellationToken); //TODO: What if fetching rate failed? if (rateResult.BidAsk is null) { @@ -199,7 +195,7 @@ namespace BTCPayServer.Controllers model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility); model.CurrentRateText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode, true); model.FiatAmount = paidCurrency; - model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.ProductInformation.Currency, true); + model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency, true); return View(model); } else @@ -219,7 +215,7 @@ namespace BTCPayServer.Controllers createPullPayment.Amount = model.CryptoAmountNow; break; case "Fiat": - createPullPayment.Currency = invoice.ProductInformation.Currency; + createPullPayment.Currency = invoice.Currency; createPullPayment.Amount = model.FiatAmount; break; default: @@ -405,7 +401,7 @@ namespace BTCPayServer.Controllers var dto = invoice.EntityToDTO(); var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var storeBlob = store.GetStoreBlob(); - var currency = invoice.ProductInformation.Currency; + var currency = invoice.Currency; var accounting = paymentMethod.Calculate(); ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled && @@ -427,12 +423,11 @@ namespace BTCPayServer.Controllers var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId]; var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits; - var model = new PaymentModel() { CryptoCode = network.CryptoCode, RootPath = this.Request.PathBase.Value.WithTrailingSlash(), - OrderId = invoice.OrderId, + OrderId = invoice.Metadata.OrderId, InvoiceId = invoice.Id, DefaultLang = storeBlob.DefaultLang ?? "en", CustomCSSLink = storeBlob.CustomCSS, @@ -442,7 +437,7 @@ namespace BTCPayServer.Controllers BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ShowMoney(divisibility), OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility), - OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation), + OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = storeBlob.RequiresRefundEmail, ShowRecommendedFee = storeBlob.ShowRecommendedFee, @@ -450,7 +445,7 @@ namespace BTCPayServer.Controllers ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, - ItemDesc = invoice.ProductInformation.ItemDesc, + ItemDesc = invoice.Metadata.ItemDesc, Rate = ExchangeRate(paymentMethod), MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/", RedirectAutomatically = invoice.RedirectAutomatically, @@ -500,7 +495,7 @@ namespace BTCPayServer.Controllers paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob); if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats") { - model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency); + model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.Currency); } model.UISettings = paymentMethodHandler.GetCheckoutUISettings(); model.PaymentMethodId = paymentMethodId.ToString(); @@ -509,17 +504,17 @@ namespace BTCPayServer.Controllers return model; } - private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation) + private string OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity) { // if invoice source currency is the same as currently display currency, no need for "order amount from invoice" - if (cryptoCode == productInformation.Currency) + if (cryptoCode == invoiceEntity.Currency) return null; - return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency); + return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency); } private string ExchangeRate(PaymentMethod paymentMethod) { - string currency = paymentMethod.ParentEntity.ProductInformation.Currency; + string currency = paymentMethod.ParentEntity.Currency; return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency); } @@ -628,9 +623,9 @@ namespace BTCPayServer.Controllers ShowCheckout = invoice.Status == InvoiceStatus.New, Date = invoice.InvoiceTime, InvoiceId = invoice.Id, - OrderId = invoice.OrderId ?? string.Empty, + OrderId = invoice.Metadata.OrderId ?? string.Empty, RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty, - AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency), + AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency), CanMarkInvalid = state.CanMarkInvalid(), CanMarkComplete = state.CanMarkComplete(), Details = InvoicePopulatePayments(invoice), @@ -732,7 +727,7 @@ namespace BTCPayServer.Controllers try { - var result = await CreateInvoiceCore(new CreateInvoiceRequest() + var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest() { Price = model.Amount.Value, Currency = model.Currency, diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index c8da6d15b..e9adc862d 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -23,9 +23,7 @@ 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 BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers @@ -74,7 +72,7 @@ namespace BTCPayServer.Controllers } - internal async Task> CreateInvoiceCore(CreateInvoiceRequest invoice, + internal async Task> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) { @@ -83,7 +81,7 @@ namespace BTCPayServer.Controllers return new DataWrapper(resp) {Facade = "pos/invoice"}; } - internal async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) + internal async Task CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) { invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; InvoiceLogs logs = new InvoiceLogs(); @@ -99,23 +97,21 @@ namespace BTCPayServer.Controllers throw new BitpayHttpException(400, "The expirationTime is set too soon"); } entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration); - entity.OrderId = invoice.OrderId; + entity.Metadata.OrderId = invoice.OrderId; entity.ServerUrl = serverUrl; entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications; entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURLTemplate = invoice.NotificationURL; entity.NotificationEmail = invoice.NotificationEmail; - entity.BuyerInformation = Map(invoice); entity.PaymentTolerance = storeBlob.PaymentTolerance; if (additionalTags != null) entity.InternalTags.AddRange(additionalTags); - //Another way of passing buyer info to support - FillBuyerInfo(invoice.Buyer, entity.BuyerInformation); - if (entity?.BuyerInformation?.BuyerEmail != null) + FillBuyerInfo(invoice, entity); + if (entity.Metadata.BuyerEmail != null) { - if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail)) + if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail)) throw new BitpayHttpException(400, "Invalid email"); - entity.RefundMail = entity.BuyerInformation.BuyerEmail; + entity.RefundMail = entity.Metadata.BuyerEmail; } var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m; @@ -132,8 +128,23 @@ namespace BTCPayServer.Controllers invoice.TaxIncluded = Math.Max(0.0m, taxIncluded); invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price); - entity.ProductInformation = Map(invoice); + entity.Metadata.ItemCode = invoice.ItemCode; + entity.Metadata.ItemDesc = invoice.ItemDesc; + entity.Metadata.Physical = invoice.Physical; + entity.Metadata.TaxIncluded = invoice.TaxIncluded; + entity.Currency = invoice.Currency; + entity.Price = invoice.Price; + if (invoice.Metadata != null) + { + var currentMetadata = entity.Metadata.ToJObject(); + foreach (var prop in invoice.Metadata.Properties()) + { + if (!currentMetadata.ContainsKey(prop.Name)) + currentMetadata.Add(prop.Name, prop.Value); + } + entity.Metadata = InvoiceMetadata.FromJObject(currentMetadata); + } entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite; @@ -220,8 +231,7 @@ namespace BTCPayServer.Controllers entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); - entity.PosData = invoice.PosData; - + entity.Metadata.PosData = invoice.PosData; foreach (var app in await getAppsTaggingStore) { entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id)); @@ -273,7 +283,7 @@ namespace BTCPayServer.Controllers var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var storeBlob = store.GetStoreBlob(); var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); - var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; + var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)]; if (rate.BidAsk == null) { return null; @@ -337,24 +347,30 @@ namespace BTCPayServer.Controllers return policy; } - private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation) + private void FillBuyerInfo(BitpayCreateInvoiceRequest req, InvoiceEntity invoiceEntity) { + var buyerInformation = invoiceEntity.Metadata; + buyerInformation.BuyerAddress1 = req.BuyerAddress1; + buyerInformation.BuyerAddress2 = req.BuyerAddress2; + buyerInformation.BuyerCity = req.BuyerCity; + buyerInformation.BuyerCountry = req.BuyerCountry; + buyerInformation.BuyerEmail = req.BuyerEmail; + buyerInformation.BuyerName = req.BuyerName; + buyerInformation.BuyerPhone = req.BuyerPhone; + buyerInformation.BuyerState = req.BuyerState; + buyerInformation.BuyerZip = req.BuyerZip; + var buyer = req.Buyer; if (buyer == null) return; - buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1; - buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2; - buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City; - buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country; - buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email; - buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name; - buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone; - buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State; - buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip; - } - - private TDest Map(TFrom data) - { - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(data)); + buyerInformation.BuyerAddress1 ??= buyer.Address1; + buyerInformation.BuyerAddress2 ??= buyer.Address2; + buyerInformation.BuyerCity ??= buyer.City; + buyerInformation.BuyerCountry ??= buyer.country; + buyerInformation.BuyerEmail ??= buyer.email; + buyerInformation.BuyerName ??= buyer.Name; + buyerInformation.BuyerPhone ??= buyer.phone; + buyerInformation.BuyerState ??= buyer.State; + buyerInformation.BuyerZip ??= buyer.zip; } } } diff --git a/BTCPayServer/Controllers/PaymentRequestController.cs b/BTCPayServer/Controllers/PaymentRequestController.cs index 3e198c14d..0c75609cb 100644 --- a/BTCPayServer/Controllers/PaymentRequestController.cs +++ b/BTCPayServer/Controllers/PaymentRequestController.cs @@ -20,7 +20,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Routing; -using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; +using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest; using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; using StoreData = BTCPayServer.Data.StoreData; @@ -260,7 +260,7 @@ namespace BTCPayServer.Controllers try { var redirectUrl = _linkGenerator.PaymentRequestLink(id, Request.Scheme, Request.Host, Request.PathBase); - var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() + var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest() { OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}", Currency = blob.Currency, diff --git a/BTCPayServer/Controllers/PublicController.cs b/BTCPayServer/Controllers/PublicController.cs index c29eb6380..dd5c81350 100644 --- a/BTCPayServer/Controllers/PublicController.cs +++ b/BTCPayServer/Controllers/PublicController.cs @@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers DataWrapper invoice = null; try { - invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest() + invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest() { Price = model.Price, Currency = model.Currency, diff --git a/BTCPayServer/Data/InvoiceDataExtensions.cs b/BTCPayServer/Data/InvoiceDataExtensions.cs index 1cfd65ea9..16bb3fd25 100644 --- a/BTCPayServer/Data/InvoiceDataExtensions.cs +++ b/BTCPayServer/Data/InvoiceDataExtensions.cs @@ -1,4 +1,5 @@ using BTCPayServer.Services.Invoices; +using Microsoft.AspNetCore.Authorization.Infrastructure; namespace BTCPayServer.Data { @@ -8,6 +9,17 @@ namespace BTCPayServer.Data { var entity = NBitcoin.JsonConverters.Serializer.ToObject(ZipUtils.Unzip(invoiceData.Blob), null); entity.Networks = networks; + if (entity.Metadata is null) + { + if (entity.Version < InvoiceEntity.GreenfieldInvoices_Version) + { + entity.MigrateLegacyInvoice(); + } + else + { + entity.Metadata = new InvoiceMetadata(); + } + } return entity; } public static InvoiceState GetInvoiceState(this InvoiceData invoiceData) diff --git a/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs b/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs index 665c5c1e4..6a89c9e13 100644 --- a/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs +++ b/BTCPayServer/HostedServices/AppInventoryUpdaterHostedService.cs @@ -104,9 +104,8 @@ namespace BTCPayServer.HostedServices return; } - - if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) || - AppService.TryParsePosCartItems(invoiceEvent.Invoice.PosData, out cartItems))) + if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) || + AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems))) { var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice); @@ -116,9 +115,9 @@ namespace BTCPayServer.HostedServices } var items = cartItems ?? new Dictionary(); - if (!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode)) + if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode)) { - items.TryAdd(invoiceEvent.Invoice.ProductInformation.ItemCode, 1); + items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1); } _eventAggregator.Publish(new UpdateAppInventory() diff --git a/BTCPayServer/Models/CreateInvoiceRequest.cs b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs similarity index 97% rename from BTCPayServer/Models/CreateInvoiceRequest.cs rename to BTCPayServer/Models/BitpayCreateInvoiceRequest.cs index 74a0b20e4..3ef626a98 100644 --- a/BTCPayServer/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs @@ -2,10 +2,11 @@ using System; using System.Collections.Generic; using NBitpayClient; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Models { - public class CreateInvoiceRequest + public class BitpayCreateInvoiceRequest { [JsonProperty(PropertyName = "buyer")] public Buyer Buyer { get; set; } @@ -81,5 +82,6 @@ namespace BTCPayServer.Models //Bitpay compatibility: create invoice in btcpay uses this instead of supportedTransactionCurrencies [JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)] public IEnumerable PaymentCurrencies { get; set; } + public JObject Metadata { get; set; } } } diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 0d9f912e8..2052f3365 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -6,8 +6,6 @@ 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 { @@ -76,22 +74,12 @@ namespace BTCPayServer.Models.InvoicingModels { get; set; } - - public string OrderId - { - get; set; - } public string RefundEmail { get; set; } public string TaxIncluded { get; set; } - public BuyerInformation BuyerInformation - { - get; - set; - } public string TransactionSpeed { get; set; } public object StoreName @@ -116,11 +104,7 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - public ProductInformation ProductInformation - { - get; - internal set; - } + public InvoiceMetadata TypedMetadata { get; set; } public AddressModel[] Addresses { get; set; } public DateTimeOffset MonitoringDate { get; internal set; } public List Events { get; internal set; } diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index e743fcbae..a7c75ae3c 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -101,9 +101,9 @@ namespace BTCPayServer.PaymentRequest Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice() { Id = entity.Id, - Amount = entity.ProductInformation.Price, - AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency), - Currency = entity.ProductInformation.Currency, + Amount = entity.Price, + AmountFormatted = _currencies.FormatCurrency(entity.Price, blob.Currency), + Currency = entity.Currency, ExpiryDate = entity.ExpirationTime.DateTime, Status = entity.GetInvoiceState().ToString(), Payments = entity diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 49ac375b8..4fb7c869c 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -48,7 +48,7 @@ namespace BTCPayServer.Payments.Lightning var storeBlob = store.GetStoreBlob(); var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network); var invoice = paymentMethod.ParentEntity; - var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility); + var due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) @@ -58,8 +58,8 @@ namespace BTCPayServer.Payments.Lightning string description = storeBlob.LightningDescriptionTemplate; description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) - .Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + .Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) { try diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 9ad30f2a4..400a334d2 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -97,8 +97,8 @@ namespace BTCPayServer.Services.Apps var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount); var perkCount = paidInvoices - .Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode)) - .GroupBy(entity => entity.ProductInformation.ItemCode) + .Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode)) + .GroupBy(entity => entity.Metadata.ItemCode) .ToDictionary(entities => entities.Key, entities => entities.Count()); var perks = Parse(settings.PerksTemplate, settings.TargetCurrency); @@ -331,12 +331,12 @@ namespace BTCPayServer.Services.Apps public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap) { var contributions = invoices - .Where(p => p.ProductInformation.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase)) + .Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase)) .SelectMany(p => { var contribution = new Contribution(); - contribution.PaymentMethodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike); - contribution.CurrencyValue = p.ProductInformation.Price; + contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike); + contribution.CurrencyValue = p.Price; contribution.Value = contribution.CurrencyValue; // For hardcap, we count newly created invoices as part of the contributions diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 81a122a45..dfa7e97ce 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -56,9 +56,8 @@ namespace BTCPayServer.Services.Invoices.Export private IEnumerable convertFromDb(InvoiceEntity invoice) { var exportList = new List(); - var currency = Currencies.GetNumberFormatInfo(invoice.ProductInformation.Currency, true); - - var invoiceDue = invoice.ProductInformation.Price; + var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true); + var invoiceDue = invoice.Price; // in this first version we are only exporting invoices that were paid foreach (var payment in invoice.GetPayments()) { @@ -88,7 +87,7 @@ namespace BTCPayServer.Services.Invoices.Export // while looking just at export you could sum Paid and assume merchant "received payments" NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture), InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits), - OrderId = invoice.OrderId, + OrderId = invoice.Metadata.OrderId ?? string.Empty, StoreId = invoice.StoreId, InvoiceId = invoice.Id, InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime, @@ -99,11 +98,11 @@ namespace BTCPayServer.Services.Invoices.Export InvoiceStatus = invoice.StatusString, InvoiceExceptionStatus = invoice.ExceptionStatusString, #pragma warning restore CS0618 // Type or member is obsolete - InvoiceItemCode = invoice.ProductInformation.ItemCode, - InvoiceItemDesc = invoice.ProductInformation.ItemDesc, - InvoicePrice = invoice.ProductInformation.Price, - InvoiceCurrency = invoice.ProductInformation.Currency, - BuyerEmail = invoice.BuyerInformation?.BuyerEmail + InvoiceItemCode = invoice.Metadata.ItemCode, + InvoiceItemDesc = invoice.Metadata.ItemDesc, + InvoicePrice = invoice.Price, + InvoiceCurrency = invoice.Currency, + BuyerEmail = invoice.Metadata.BuyerEmail }; exportList.Add(target); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 72730ed96..aff8665a4 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using Amazon.Runtime.Internal.Util; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.JsonConverters; @@ -9,17 +10,31 @@ using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.CodeAnalysis; using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; using NBXplorer; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using YamlDotNet.Core.Tokens; +using YamlDotNet.Serialization.NamingConventions; namespace BTCPayServer.Services.Invoices { - public class BuyerInformation + public class InvoiceMetadata { + public static readonly JsonSerializer MetadataSerializer; + static InvoiceMetadata() + { + var seria = new JsonSerializer(); + seria.DefaultValueHandling = DefaultValueHandling.Ignore; + seria.FloatParseHandling = FloatParseHandling.Decimal; + seria.ContractResolver = new CamelCasePropertyNamesContractResolver(); + MetadataSerializer = seria; + } + public string OrderId { get; set; } [JsonProperty(PropertyName = "buyerName")] public string BuyerName { @@ -66,10 +81,7 @@ namespace BTCPayServer.Services.Invoices { get; set; } - } - public class ProductInformation - { [JsonProperty(PropertyName = "itemDesc")] public string ItemDesc { @@ -81,35 +93,122 @@ namespace BTCPayServer.Services.Invoices get; set; } [JsonProperty(PropertyName = "physical")] - public bool Physical - { - get; set; - } - - [JsonProperty(PropertyName = "price")] - public decimal Price + public bool? Physical { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] - public decimal TaxIncluded + public decimal? TaxIncluded { get; set; } + public string PosData { get; set; } + [JsonExtensionData] + public IDictionary AdditionalData { get; set; } - [JsonProperty(PropertyName = "currency")] - public string Currency + public static InvoiceMetadata FromJObject(JObject jObject) { - get; set; + return jObject.ToObject(MetadataSerializer); + } + public JObject ToJObject() + { + return JObject.FromObject(this, MetadataSerializer); } } + public class InvoiceEntity { + 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; + } + } + class ProductInformation + { + [JsonProperty(PropertyName = "itemDesc")] + public string ItemDesc + { + get; set; + } + [JsonProperty(PropertyName = "itemCode")] + public string ItemCode + { + get; set; + } + [JsonProperty(PropertyName = "physical")] + public bool Physical + { + get; set; + } + + [JsonProperty(PropertyName = "price")] + public decimal Price + { + get; set; + } + + [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] + public decimal TaxIncluded + { + get; set; + } + + [JsonProperty(PropertyName = "currency")] + public string Currency + { + get; set; + } + } [JsonIgnore] public BTCPayNetworkProvider Networks { get; set; } public const int InternalTagSupport_Version = 1; - public const int Lastest_Version = 1; + public const int GreenfieldInvoices_Version = 2; + public const int Lastest_Version = 2; public int Version { get; set; } public string Id { @@ -120,11 +219,6 @@ namespace BTCPayServer.Services.Invoices get; set; } - public string OrderId - { - get; set; - } - public SpeedPolicy SpeedPolicy { get; set; @@ -148,20 +242,20 @@ namespace BTCPayServer.Services.Invoices { get; set; } - public ProductInformation ProductInformation - { - get; set; - } - public BuyerInformation BuyerInformation - { - get; set; - } - public string PosData + + public InvoiceMetadata Metadata { get; set; } + + public decimal Price { get; set; } + public string Currency { get; set; } + + [JsonExtensionData] + public IDictionary AdditionalData { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public HashSet InternalTags { get; set; } = new HashSet(); @@ -300,7 +394,7 @@ namespace BTCPayServer.Services.Invoices private Uri FillPlaceholdersUri(string v) { - var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(OrderId) ?? "", StringComparison.OrdinalIgnoreCase) + var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(Metadata.OrderId) ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{InvoiceId}", System.Web.HttpUtility.UrlEncode(Id) ?? "", StringComparison.OrdinalIgnoreCase); if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https")) return uri; @@ -383,8 +477,8 @@ namespace BTCPayServer.Services.Invoices { Id = Id, StoreId = StoreId, - OrderId = OrderId, - PosData = PosData, + OrderId = Metadata.OrderId, + PosData = Metadata.PosData, CurrentTime = DateTimeOffset.UtcNow, InvoiceTime = InvoiceTime, ExpirationTime = ExpirationTime, @@ -392,7 +486,7 @@ namespace BTCPayServer.Services.Invoices Status = StatusString, ExceptionStatus = ExceptionStatus == InvoiceExceptionStatus.None ? new JValue(false) : new JValue(ExceptionStatusString), #pragma warning restore CS0618 // Type or member is obsolete - Currency = ProductInformation.Currency, + Currency = Currency, Flags = new Flags() { Refundable = Refundable }, PaymentSubtotals = new Dictionary(), PaymentTotals = new Dictionary(), @@ -415,7 +509,7 @@ namespace BTCPayServer.Services.Invoices var address = details?.GetPaymentDestination(); var exrates = new Dictionary { - { ProductInformation.Currency, cryptoInfo.Rate } + { Currency, cryptoInfo.Rate } }; cryptoInfo.CryptoCode = cryptoCode; @@ -502,29 +596,27 @@ namespace BTCPayServer.Services.Invoices //dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice - Populate(ProductInformation, dto); + dto.ItemCode = Metadata.ItemCode; + dto.ItemDesc = Metadata.ItemDesc; + dto.TaxIncluded = Metadata.TaxIncluded ?? 0m; + dto.Price = Price; + dto.Currency = Currency; dto.Buyer = new JObject(); - dto.Buyer.Add(new JProperty("name", BuyerInformation.BuyerName)); - dto.Buyer.Add(new JProperty("address1", BuyerInformation.BuyerAddress1)); - dto.Buyer.Add(new JProperty("address2", BuyerInformation.BuyerAddress2)); - dto.Buyer.Add(new JProperty("locality", BuyerInformation.BuyerCity)); - dto.Buyer.Add(new JProperty("region", BuyerInformation.BuyerState)); - dto.Buyer.Add(new JProperty("postalCode", BuyerInformation.BuyerZip)); - dto.Buyer.Add(new JProperty("country", BuyerInformation.BuyerCountry)); - dto.Buyer.Add(new JProperty("phone", BuyerInformation.BuyerPhone)); - dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(BuyerInformation.BuyerEmail) ? RefundMail : BuyerInformation.BuyerEmail)); + dto.Buyer.Add(new JProperty("name", Metadata.BuyerName)); + dto.Buyer.Add(new JProperty("address1", Metadata.BuyerAddress1)); + dto.Buyer.Add(new JProperty("address2", Metadata.BuyerAddress2)); + dto.Buyer.Add(new JProperty("locality", Metadata.BuyerCity)); + dto.Buyer.Add(new JProperty("region", Metadata.BuyerState)); + dto.Buyer.Add(new JProperty("postalCode", Metadata.BuyerZip)); + dto.Buyer.Add(new JProperty("country", Metadata.BuyerCountry)); + dto.Buyer.Add(new JProperty("phone", Metadata.BuyerPhone)); + dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(Metadata.BuyerEmail) ? RefundMail : Metadata.BuyerEmail)); dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for dto.Guid = Guid.NewGuid().ToString(); return dto; } - private void Populate(TFrom from, TDest dest) - { - var str = JsonConvert.SerializeObject(from); - JsonConvert.PopulateObject(str, dest); - } - internal bool Support(PaymentMethodId paymentMethodId) { var rates = GetPaymentMethods(); @@ -602,6 +694,62 @@ namespace BTCPayServer.Services.Invoices { return new InvoiceState(Status, ExceptionStatus); } + + /// + /// Invoice version < 1 were saving metadata directly under the InvoiceEntity + /// object. But in version > 2, the metadata is saved under the InvoiceEntity.Metadata object + /// This method is extracting metadata from the InvoiceEntity of version < 1 invoices and put them in InvoiceEntity.Metadata. + /// + internal void MigrateLegacyInvoice() + { + T TryParseMetadata(string field) where T : class + { + if (AdditionalData.TryGetValue(field, out var token) && token is JObject obj) + { + return obj.ToObject(); + } + return null; + } + if (TryParseMetadata("buyerInformation") is BuyerInformation buyerInformation && + TryParseMetadata("productInformation") is ProductInformation productInformation) + { + var wellknown = new InvoiceMetadata() + { + BuyerAddress1 = buyerInformation.BuyerAddress1, + BuyerAddress2 = buyerInformation.BuyerAddress2, + BuyerCity = buyerInformation.BuyerCity, + BuyerCountry = buyerInformation.BuyerCountry, + BuyerEmail = buyerInformation.BuyerEmail, + BuyerName = buyerInformation.BuyerName, + BuyerPhone = buyerInformation.BuyerPhone, + BuyerState = buyerInformation.BuyerState, + BuyerZip = buyerInformation.BuyerZip, + ItemCode = productInformation.ItemCode, + ItemDesc = productInformation.ItemDesc, + Physical = productInformation.Physical, + TaxIncluded = productInformation.TaxIncluded + }; + if (AdditionalData.TryGetValue("posData", out var token) && + token is JValue val && + val.Type == JTokenType.String) + { + wellknown.PosData = val.Value(); + } + if (AdditionalData.TryGetValue("orderId", out var token2) && + token2 is JValue val2 && + val2.Type == JTokenType.String) + { + wellknown.OrderId = val2.Value(); + } + Metadata = wellknown; + Currency = productInformation.Currency; + Price = productInformation.Price; + } + else + { + throw new InvalidOperationException("Not a legacy invoice"); + } + } } @@ -836,7 +984,7 @@ namespace BTCPayServer.Services.Invoices paymentPredicate = paymentPredicate ?? new Func((p) => true); var paymentMethods = ParentEntity.GetPaymentMethods(); - var totalDue = ParentEntity.ProductInformation.Price / Rate; + var totalDue = ParentEntity.Price / Rate; var paid = 0m; var cryptoPaid = 0.0m; diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index d65ba35ca..b5d479b80 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Encoders = NBitcoin.DataEncoders.Encoders; using InvoiceData = BTCPayServer.Data.InvoiceData; @@ -65,6 +66,7 @@ retry: Networks = _Networks, Version = InvoiceEntity.Lastest_Version, InvoiceTime = DateTimeOffset.UtcNow, + Metadata = new InvoiceMetadata() }; } @@ -162,11 +164,11 @@ retry: Id = invoice.Id, Created = invoice.InvoiceTime, Blob = ToBytes(invoice, null), - OrderId = invoice.OrderId, + OrderId = invoice.Metadata.OrderId, #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete - ItemCode = invoice.ProductInformation.ItemCode, + ItemCode = invoice.Metadata.ItemCode, CustomerEmail = invoice.RefundMail, Archived = false }); @@ -198,10 +200,9 @@ retry: textSearch.Add(invoice.Id); textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture)); - textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)); - textSearch.Add(invoice.OrderId); - textSearch.Add(ToString(invoice.BuyerInformation, null)); - textSearch.Add(ToString(invoice.ProductInformation, null)); + textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture)); + textSearch.Add(invoice.Metadata.OrderId); + textSearch.Add(ToString(invoice.Metadata, null)); textSearch.Add(invoice.StoreId); AddToTextSearch(invoice.Id, textSearch.ToArray()); @@ -555,10 +556,9 @@ retry: { entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList(); } - - if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.BuyerInformation.BuyerEmail)) + if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.Metadata.BuyerEmail)) { - entity.BuyerInformation.BuyerEmail = entity.RefundMail; + entity.Metadata.BuyerEmail = entity.RefundMail; } entity.Archived = invoice.Archived; return entity; diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index d9b29692f..b87574944 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -1,4 +1,4 @@ -@model InvoiceDetailsModel +@model InvoiceDetailsModel @{ ViewData["Title"] = "Invoice " + Model.Id; } @@ -88,7 +88,7 @@ Order Id - @Model.OrderId + @Model.TypedMetadata.OrderId Total fiat due @@ -109,39 +109,39 @@ - + - + - + - + - + - + - + - + - +
Name@Model.BuyerInformation.BuyerName@Model.TypedMetadata.BuyerName
Email@Model.BuyerInformation.BuyerEmail@Model.TypedMetadata.BuyerEmail
Phone@Model.BuyerInformation.BuyerPhone@Model.TypedMetadata.BuyerPhone
Address 1@Model.BuyerInformation.BuyerAddress1@Model.TypedMetadata.BuyerAddress1
Address 2@Model.BuyerInformation.BuyerAddress2@Model.TypedMetadata.BuyerAddress2
City@Model.BuyerInformation.BuyerCity@Model.TypedMetadata.BuyerCity
State@Model.BuyerInformation.BuyerState@Model.TypedMetadata.BuyerState
Country@Model.BuyerInformation.BuyerCountry@Model.TypedMetadata.BuyerCountry
Zip@Model.BuyerInformation.BuyerZip@Model.TypedMetadata.BuyerZip
@if (Model.PosData.Count == 0) @@ -150,11 +150,11 @@ - + - + @@ -177,11 +177,11 @@
Item code@Model.ProductInformation.ItemCode@Model.TypedMetadata.ItemCode
Item Description@Model.ProductInformation.ItemDesc@Model.TypedMetadata.ItemDesc
Price
- + - + From 67b04473b592ccb03cd7fd323c03f42a69c53d4c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Aug 2020 14:01:39 +0900 Subject: [PATCH 11/16] Decouple the CreateInvoiceCore from BitpayCreateInvoice, remove some features from greenfield invoice for now --- .../JsonConverters/TimeSpanJsonConverter.cs | 33 ++++- .../Models/CreateInvoiceRequest.cs | 12 +- .../Models/CreateLightningInvoiceRequest.cs | 2 +- .../Models/CreatePullPaymentRequest.cs | 2 +- .../Models/PullPaymentBaseData.cs | 2 +- BTCPayServer.Client/Models/StoreBaseData.cs | 4 +- .../GreenField/InvoiceController.cs | 58 ++------- .../GreenField/StoresController.cs | 8 +- BTCPayServer/Controllers/InvoiceController.cs | 114 +++++++++++------- BTCPayServer/Controllers/StoresController.cs | 8 +- BTCPayServer/Data/PullPaymentsExtensions.cs | 2 +- BTCPayServer/Data/StoreBlob.cs | 15 ++- .../Models/BitpayCreateInvoiceRequest.cs | 1 - 13 files changed, 135 insertions(+), 126 deletions(-) diff --git a/BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs b/BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs index 621fb783a..b392655d4 100644 --- a/BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs +++ b/BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs @@ -4,13 +4,38 @@ using Newtonsoft.Json; namespace BTCPayServer.Client.JsonConverters { - public class TimeSpanJsonConverter : JsonConverter + public abstract class TimeSpanJsonConverter : JsonConverter { + public class Seconds : TimeSpanJsonConverter + { + protected override long ToLong(TimeSpan value) + { + return (long)value.TotalSeconds; + } + + protected override TimeSpan ToTimespan(long value) + { + return TimeSpan.FromSeconds(value); + } + } + public class Minutes : TimeSpanJsonConverter + { + protected override long ToLong(TimeSpan value) + { + return (long)value.TotalMinutes; + } + protected override TimeSpan ToTimespan(long value) + { + return TimeSpan.FromMinutes(value); + } + } public override bool CanConvert(Type objectType) { return objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?); } + protected abstract TimeSpan ToTimespan(long value); + protected abstract long ToLong(TimeSpan value); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { try @@ -24,11 +49,11 @@ namespace BTCPayServer.Client.JsonConverters } if (reader.TokenType != JsonToken.Integer) throw new JsonObjectException("Invalid timespan, expected integer", reader); - return TimeSpan.FromSeconds((long)reader.Value); + return ToTimespan((long)reader.Value); } catch { - throw new JsonObjectException("Invalid locktime", reader); + throw new JsonObjectException("Invalid timespan", reader); } } @@ -36,7 +61,7 @@ namespace BTCPayServer.Client.JsonConverters { if (value is TimeSpan s) { - writer.WriteValue((long)s.TotalSeconds); + writer.WriteValue(ToLong(s)); } } } diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index 621993687..fddf81bda 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -1,4 +1,5 @@ using System; +using BTCPayServer.Client.JsonConverters; using BTCPayServer.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -20,12 +21,13 @@ namespace BTCPayServer.Client.Models 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; } + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + [JsonProperty("expirationMinutes")] + public TimeSpan? Expiration { get; set; } + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + [JsonProperty("monitoringMinutes")] + public TimeSpan? Monitoring { get; set; } [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public double? PaymentTolerance { get; set; } diff --git a/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs index 4282b422c..ba38843fa 100644 --- a/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs @@ -20,7 +20,7 @@ namespace BTCPayServer.Client.Models [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } public string Description { get; set; } - [JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))] + [JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))] public TimeSpan Expiry { get; set; } public bool PrivateRouteHints { get; set; } diff --git a/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs b/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs index a314573ad..6cce582ca 100644 --- a/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs +++ b/BTCPayServer.Client/Models/CreatePullPaymentRequest.cs @@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } public string Currency { get; set; } - [JsonConverter(typeof(TimeSpanJsonConverter))] + [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] public TimeSpan? Period { get; set; } [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset? ExpiresAt { get; set; } diff --git a/BTCPayServer.Client/Models/PullPaymentBaseData.cs b/BTCPayServer.Client/Models/PullPaymentBaseData.cs index d95b60e06..371bdee9d 100644 --- a/BTCPayServer.Client/Models/PullPaymentBaseData.cs +++ b/BTCPayServer.Client/Models/PullPaymentBaseData.cs @@ -16,7 +16,7 @@ namespace BTCPayServer.Client.Models public string Currency { get; set; } [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Amount { get; set; } - [JsonConverter(typeof(TimeSpanJsonConverter))] + [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] public TimeSpan? Period { get; set; } public bool Archived { get; set; } public string ViewLink { get; set; } diff --git a/BTCPayServer.Client/Models/StoreBaseData.cs b/BTCPayServer.Client/Models/StoreBaseData.cs index 33cb6c00f..61e8ed747 100644 --- a/BTCPayServer.Client/Models/StoreBaseData.cs +++ b/BTCPayServer.Client/Models/StoreBaseData.cs @@ -16,11 +16,11 @@ namespace BTCPayServer.Client.Models public string Website { get; set; } - [JsonConverter(typeof(TimeSpanJsonConverter))] + [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15); - [JsonConverter(typeof(TimeSpanJsonConverter))] + [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60); diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 05aebc392..9fb31470b 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -125,10 +125,10 @@ namespace BTCPayServer.Controllers.GreenField } } - if (request.Checkout.ExpirationTime != null && request.Checkout.ExpirationTime < DateTime.Now) + if (request.Checkout.Expiration != null && request.Checkout.Expiration < TimeSpan.FromSeconds(30.0)) { - request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExpirationTime, - "Expiration time must be in the future", this); + request.AddModelError(invoiceRequest => invoiceRequest.Checkout.Expiration, + "Expiration time must be at least 30 seconds", this); } if (request.Checkout.PaymentTolerance != null && @@ -143,7 +143,7 @@ namespace BTCPayServer.Controllers.GreenField try { - var invoice = await _invoiceController.CreateInvoiceCoreRaw(FromModel(request), store, + var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteUri("")); return Ok(ToModel(invoice)); } @@ -263,14 +263,12 @@ namespace BTCPayServer.Controllers.GreenField Metadata = entity.Metadata.ToJObject(), Checkout = new CreateInvoiceRequest.CheckoutOptions() { - ExpirationTime = entity.ExpirationTime, + Expiration = entity.ExpirationTime - entity.InvoiceTime, + Monitoring = entity.MonitoringExpiration - 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 + SpeedPolicy = entity.SpeedPolicy }, PaymentMethodData = entity.GetPaymentMethods().ToDictionary(method => method.GetId().ToString(), method => @@ -317,47 +315,5 @@ namespace BTCPayServer.Controllers.GreenField }) }; } - - private Models.BitpayCreateInvoiceRequest FromModel(CreateInvoiceRequest entity) - { - InvoiceMetadata invoiceMetadata = null; - if (entity.Metadata != null) - { - invoiceMetadata = entity.Metadata.ToObject(); - } - return new Models.BitpayCreateInvoiceRequest() - { - Buyer = invoiceMetadata == null ? null : new Buyer() - { - Address1 = invoiceMetadata.BuyerAddress1, - Address2 = invoiceMetadata.BuyerAddress2, - City = invoiceMetadata.BuyerCity, - country = invoiceMetadata.BuyerCountry, - email = invoiceMetadata.BuyerEmail, - Name = invoiceMetadata.BuyerName, - phone = invoiceMetadata.BuyerPhone, - State = invoiceMetadata.BuyerState, - zip = invoiceMetadata.BuyerZip, - }, - Currency = entity.Currency, - Price = entity.Amount, - Refundable = true, - ExtendedNotifications = true, - FullNotifications = true, - RedirectURL = entity.Checkout.RedirectUri, - RedirectAutomatically = entity.Checkout.RedirectAutomatically, - ExpirationTime = entity.Checkout.ExpirationTime, - TransactionSpeed = entity.Checkout.SpeedPolicy?.ToString(), - PaymentCurrencies = entity.Checkout.PaymentMethods, - NotificationURL = entity.Checkout.RedirectUri, - PosData = invoiceMetadata?.PosData, - Physical = invoiceMetadata?.Physical ?? false, - ItemCode = invoiceMetadata?.ItemCode, - ItemDesc = invoiceMetadata?.ItemDesc, - TaxIncluded = invoiceMetadata?.TaxIncluded, - OrderId = invoiceMetadata?.OrderId, - Metadata = entity.Metadata - }; - } } } diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index 8ae2a6199..25b59fd82 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -124,8 +124,8 @@ namespace BTCPayServer.Controllers.GreenField ShowRecommendedFee = storeBlob.ShowRecommendedFee, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, DefaultLang = storeBlob.DefaultLang, - MonitoringExpiration = TimeSpan.FromMinutes(storeBlob.MonitoringExpiration), - InvoiceExpiration = TimeSpan.FromMinutes(storeBlob.InvoiceExpiration), + MonitoringExpiration = storeBlob.MonitoringExpiration, + InvoiceExpiration = storeBlob.InvoiceExpiration, LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, CustomLogo = storeBlob.CustomLogo, CustomCSS = storeBlob.CustomCSS, @@ -160,8 +160,8 @@ namespace BTCPayServer.Controllers.GreenField blob.ShowRecommendedFee = restModel.ShowRecommendedFee; blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget; blob.DefaultLang = restModel.DefaultLang; - blob.MonitoringExpiration = (int)restModel.MonitoringExpiration.TotalMinutes; - blob.InvoiceExpiration = (int)restModel.InvoiceExpiration.TotalMinutes; + blob.MonitoringExpiration = restModel.MonitoringExpiration; + blob.InvoiceExpiration = restModel.InvoiceExpiration; blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; blob.CustomLogo = restModel.CustomLogo; blob.CustomCSS = restModel.CustomCSS; diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index e9adc862d..a9a2a00a4 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -21,6 +21,7 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Validation; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.CSharp.Syntax; using NBitpayClient; using Newtonsoft.Json; using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest; @@ -78,41 +79,31 @@ namespace BTCPayServer.Controllers { var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken); var resp = entity.EntityToDTO(); - return new DataWrapper(resp) {Facade = "pos/invoice"}; + return new DataWrapper(resp) { Facade = "pos/invoice" }; } internal async Task CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) { - invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; - InvoiceLogs logs = new InvoiceLogs(); - logs.Write("Creation of invoice starting"); - var entity = _InvoiceRepository.CreateNewInvoice(); - - var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); var storeBlob = store.GetStoreBlob(); - EmailAddressAttribute emailValidator = new EmailAddressAttribute(); - entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration); + var entity = _InvoiceRepository.CreateNewInvoice(); + entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration; + entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration; if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) { throw new BitpayHttpException(400, "The expirationTime is set too soon"); } - entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration); + invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; + entity.Currency = invoice.Currency; entity.Metadata.OrderId = invoice.OrderId; + entity.Metadata.PosData = invoice.PosData; entity.ServerUrl = serverUrl; entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications; entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURLTemplate = invoice.NotificationURL; entity.NotificationEmail = invoice.NotificationEmail; - entity.PaymentTolerance = storeBlob.PaymentTolerance; if (additionalTags != null) entity.InternalTags.AddRange(additionalTags); FillBuyerInfo(invoice, entity); - if (entity.Metadata.BuyerEmail != null) - { - if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail)) - throw new BitpayHttpException(400, "Invalid email"); - entity.RefundMail = entity.Metadata.BuyerEmail; - } var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m; @@ -127,7 +118,6 @@ namespace BTCPayServer.Controllers invoice.Price = Math.Max(0.0m, invoice.Price); invoice.TaxIncluded = Math.Max(0.0m, taxIncluded); invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price); - entity.Metadata.ItemCode = invoice.ItemCode; entity.Metadata.ItemDesc = invoice.ItemDesc; entity.Metadata.Physical = invoice.Physical; @@ -135,29 +125,12 @@ namespace BTCPayServer.Controllers entity.Currency = invoice.Currency; entity.Price = invoice.Price; - if (invoice.Metadata != null) - { - var currentMetadata = entity.Metadata.ToJObject(); - foreach (var prop in invoice.Metadata.Properties()) - { - if (!currentMetadata.ContainsKey(prop.Name)) - currentMetadata.Add(prop.Name, prop.Value); - } - entity.Metadata = InvoiceMetadata.FromJObject(currentMetadata); - } - entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite; - entity.RedirectAutomatically = invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically); - - entity.Status = InvoiceStatus.New; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); - HashSet currencyPairsToFetch = new HashSet(); - var rules = storeBlob.GetRateRules(_NetworkProvider); - var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() - + IPaymentFilter excludeFilter = null; if (invoice.PaymentCurrencies?.Any() is true) { invoice.SupportedTransactionCurrencies ??= @@ -173,17 +146,67 @@ namespace BTCPayServer.Controllers var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies .Where(c => c.Value.Enabled) .Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null) + .Where(c => c != null) .ToHashSet(); - excludeFilter = PaymentFilter.Or(excludeFilter, - PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p))); + excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); } + entity.PaymentTolerance = storeBlob.PaymentTolerance; + return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken); + } + internal async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List additionalTags = null, CancellationToken cancellationToken = default) + { + var storeBlob = store.GetStoreBlob(); + var entity = _InvoiceRepository.CreateNewInvoice(); + entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration); + entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration); + if (invoice.Metadata != null) + entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata); + invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions(); + entity.Currency = invoice.Currency; + entity.Price = invoice.Amount; + entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy; + IPaymentFilter excludeFilter = null; + if (invoice.Checkout.PaymentMethods != null) + { + var supportedTransactionCurrencies = invoice.Checkout.PaymentMethods + .Select(c => PaymentMethodId.TryParse(c, out var p) ? p : null) + .ToHashSet(); + excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); + } + entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance; + return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken); + } + + internal async Task CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default) + { + InvoiceLogs logs = new InvoiceLogs(); + logs.Write("Creation of invoice starting"); + + var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); + var storeBlob = store.GetStoreBlob(); + + if (entity.Metadata.BuyerEmail != null) + { + if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail)) + throw new BitpayHttpException(400, "Invalid email"); + entity.RefundMail = entity.Metadata.BuyerEmail; + } + entity.Status = InvoiceStatus.New; + HashSet currencyPairsToFetch = new HashSet(); + var rules = storeBlob.GetRateRules(_NetworkProvider); + var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() + if (invoicePaymentMethodFilter != null) + { + excludeFilter = PaymentFilter.Or(excludeFilter, + invoicePaymentMethodFilter); + } foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId)) .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) .Where(c => c != null)) { - currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency)); + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency)); //TODO: abstract if (storeBlob.LightningMaxValue != null) currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency)); @@ -195,7 +218,12 @@ namespace BTCPayServer.Controllers var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); - var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) + List supported = new List(); + var paymentMethods = new PaymentMethodDictionary(); + + // This loop ends with .ToList so we are querying all payment methods at once + // instead of sequentially to improve response time + foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId)) .Select(c => (Handler: _paymentMethodHandlerDictionary[c.PaymentId], @@ -205,10 +233,7 @@ namespace BTCPayServer.Controllers .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs))) - .ToList(); - List supported = new List(); - var paymentMethods = new PaymentMethodDictionary(); - foreach (var o in supportedPaymentMethods) + .ToList()) { var paymentMethod = await o.PaymentMethod; if (paymentMethod == null) @@ -231,7 +256,6 @@ namespace BTCPayServer.Controllers entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); - entity.Metadata.PosData = invoice.PosData; foreach (var app in await getAppsTaggingStore) { entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id)); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 99384dcde..6392f1564 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -481,8 +481,8 @@ namespace BTCPayServer.Controllers vm.SpeedPolicy = store.SpeedPolicy; vm.CanDelete = _Repo.CanDeleteStores(); AddPaymentMethods(store, storeBlob, vm); - vm.MonitoringExpiration = storeBlob.MonitoringExpiration; - vm.InvoiceExpiration = storeBlob.InvoiceExpiration; + vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes; + vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; vm.PaymentTolerance = storeBlob.PaymentTolerance; vm.PayJoinEnabled = storeBlob.PayJoinEnabled; @@ -579,8 +579,8 @@ namespace BTCPayServer.Controllers var blob = CurrentStore.GetStoreBlob(); blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; blob.NetworkFeeMode = model.NetworkFeeMode; - blob.MonitoringExpiration = model.MonitoringExpiration; - blob.InvoiceExpiration = model.InvoiceExpiration; + blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration); + blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration); blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty; blob.PaymentTolerance = model.PaymentTolerance; var payjoinChanged = blob.PayJoinEnabled != model.PayJoinEnabled; diff --git a/BTCPayServer/Data/PullPaymentsExtensions.cs b/BTCPayServer/Data/PullPaymentsExtensions.cs index b29e872b8..eddb1080e 100644 --- a/BTCPayServer/Data/PullPaymentsExtensions.cs +++ b/BTCPayServer/Data/PullPaymentsExtensions.cs @@ -195,7 +195,7 @@ namespace BTCPayServer.Data [JsonConverter(typeof(NumericStringJsonConverter))] public decimal MinimumClaim { get; set; } public PullPaymentView View { get; set; } = new PullPaymentView(); - [JsonConverter(typeof(TimeSpanJsonConverter))] + [JsonConverter(typeof(TimeSpanJsonConverter.Seconds))] public TimeSpan? Period { get; set; } [JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))] diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index 44d4267c5..1c76b6421 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; +using BTCPayServer.Client.JsonConverters; using BTCPayServer.Client.Models; using BTCPayServer.JsonConverters; using BTCPayServer.Payments; @@ -19,8 +20,8 @@ namespace BTCPayServer.Data { public StoreBlob() { - InvoiceExpiration = 15; - MonitoringExpiration = 1440; + InvoiceExpiration = TimeSpan.FromMinutes(15); + MonitoringExpiration = TimeSpan.FromDays(1); PaymentTolerance = 0; ShowRecommendedFee = true; RecommendedFeeBlockTarget = 1; @@ -66,17 +67,19 @@ namespace BTCPayServer.Data } public string DefaultLang { get; set; } - [DefaultValue(60)] + [DefaultValue(typeof(TimeSpan), "1.00:00:00")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int MonitoringExpiration + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + public TimeSpan MonitoringExpiration { get; set; } - [DefaultValue(15)] + [DefaultValue(typeof(TimeSpan), "00:15:00")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int InvoiceExpiration + [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] + public TimeSpan InvoiceExpiration { get; set; diff --git a/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs index 3ef626a98..d5112a369 100644 --- a/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs +++ b/BTCPayServer/Models/BitpayCreateInvoiceRequest.cs @@ -82,6 +82,5 @@ namespace BTCPayServer.Models //Bitpay compatibility: create invoice in btcpay uses this instead of supportedTransactionCurrencies [JsonProperty(PropertyName = "paymentCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)] public IEnumerable PaymentCurrencies { get; set; } - public JObject Metadata { get; set; } } } From 8f0cc9de03e51017f13859766b2e92f17072c64b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Aug 2020 20:02:18 +0900 Subject: [PATCH 12/16] Update swagger --- .../Models/CreateInvoiceRequest.cs | 1 - .../swagger/v1/swagger.template.invoices.json | 116 ++---------------- 2 files changed, 10 insertions(+), 107 deletions(-) diff --git a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs index fddf81bda..c37f6a1b7 100644 --- a/BTCPayServer.Client/Models/CreateInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateInvoiceRequest.cs @@ -29,7 +29,6 @@ namespace BTCPayServer.Client.Models [JsonProperty("monitoringMinutes")] public TimeSpan? Monitoring { get; set; } - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] public double? PaymentTolerance { get; set; } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index c3dc3c6c7..5cc3d1688 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -660,12 +660,6 @@ "format": "decimal", "description": "The amount of the invoice" }, - "customerEmail": { - "type": "string", - "format": "email", - "nullable": true, - "description": "The email of the customer. If the store is configured to ask for a refund email, the checkout UI will prompt for one when not provided." - }, "currency": { "type": "string", "nullable": true, @@ -674,7 +668,7 @@ "metadata": { "type": "string", "nullable": true, - "description": "Additional information around the invoice that can be supplied.
You can pass a json body with values of `ProductInformation` `BuyerInformation` which will populate the fields to stay compatible with previous workflows (and bitpay invoice api)./>" + "description": "Additional information around the invoice that can be supplied." }, "checkout": { "nullable": true, @@ -687,84 +681,6 @@ } } }, - "ProductInformation": { - "type": "object", - "additionalProperties": false, - "properties": { - "orderId": { - "type": "string", - "nullable": true, - "description": "Can be used by the merchant to assign their own internal ID to an invoice." - }, - "posData": { - "type": "string", - "nullable": true, - "description": "A passthru variable provided by the merchant and designed to be used by the merchant to correlate the invoice with an order or other object in their system. This passthru variable can be a serialized object, e.g.:" - }, - "itemDesc": { - "type": "string", - "nullable": true, - "description": "Invoice description - will be added as a line item on the checkout page, under the merchant name" - }, - "itemCode": { - "type": "string", - "nullable": true, - "description": "An item code, for a specific product code that is being paid for. Usually used in conjunction with the POS/Crowdfund apps" - }, - "physical": { - "type": "boolean", - "description": "Whether it was a physical purchase" - }, - "taxIncluded": { - "type": "string", - "format": "decimal", - "nullable": true, - "description": "How much tax is included in the invoice amount" - } - } - }, - "BuyerInformation": { - "type": "object", - "additionalProperties": false, - "properties": { - "buyerName": { - "type": "string", - "nullable": true - }, - "buyerEmail": { - "type": "string", - "nullable": true - }, - "buyerCountry": { - "type": "string", - "nullable": true - }, - "buyerZip": { - "type": "string", - "nullable": true - }, - "buyerState": { - "type": "string", - "nullable": true - }, - "buyerCity": { - "type": "string", - "nullable": true - }, - "buyerAddress2": { - "type": "string", - "nullable": true - }, - "buyerAddress1": { - "type": "string", - "nullable": true - }, - "buyerPhone": { - "type": "string", - "nullable": true - } - } - }, "CheckoutOptions": { "type": "object", "additionalProperties": false, @@ -784,37 +700,25 @@ "items": { "type": "string" }, - "description": "A specific set of payment methods to use for this invoice" + "description": "A specific set of payment methods to use for this invoice" }, - "redirectAutomatically": { - "type": "boolean", + "expirationMinutes": { + "type": "integer", "nullable": true, - "description": "Whether to redirect to the redirectUri automatically after the invoice has been paid. Default to the store setting (which is `false` by default)" + "description": "The number of minutes after which an invoice becomes expired. Default to the store's settings. (The default store settings is 15)" }, - "redirectUri": { - "type": "string", + "monitoringMinutes": { + "type": "integer", "nullable": true, - "description": "This is the URL for a return link that is displayed on the receipt, to return the shopper back to your website after a successful purchase. This could be a page specific to the order, or to their account." - }, - "webHook": { - "type": "string", - "format": "uri", - "nullable": true, - "description":"A URL to send webhook notification to. Sent when the status changes or a payment is detected" - }, - "expirationTime": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the invoice expires. By default will use the store settings (which is set to 15 minutes by default)" + "description": "The number of minutes after an invoice expired after which we are still monitoring for incoming payments. Default to the store's settings. (The default store settings is 1440, 1 day)" }, "paymentTolerance": { - "type": "string", + "type": "number", "format": "double", "nullable": true, "minimum": 0, "maximum": 100, - "description": "A percentage dtermining whether to count the invoice as paid when the invoice is paid within the specified margin of error" + "description": "A percentage determining whether to count the invoice as paid when the invoice is paid within the specified margin of error. Default to the store's settings. (The default store settings is 100)" } } }, From be21e7d253676443e99bca7f6e7e578a92ea5d53 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Aug 2020 20:51:51 +0900 Subject: [PATCH 13/16] Remove PaymentMethodData from invoice entity --- BTCPayServer.Client/Models/InvoiceData.cs | 55 -------- BTCPayServer/BTCPayServer.csproj | 2 +- .../GreenField/InvoiceController.cs | 45 +------ .../swagger/v1/swagger.template.invoices.json | 117 +----------------- 4 files changed, 4 insertions(+), 215 deletions(-) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index cb30793f6..21209af29 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -13,60 +13,5 @@ namespace BTCPayServer.Client.Models public InvoiceStatus Status { get; set; } [JsonConverter(typeof(StringEnumConverter))] public InvoiceExceptionStatus AdditionalStatus { get; set; } - public Dictionary PaymentMethodData { get; set; } - - public class PaymentMethodDataModel - { - public string Destination { get; set; } - public string PaymentLink { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - public decimal Rate { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - public decimal PaymentMethodPaid { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - public decimal TotalPaid { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - public decimal Due { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - public decimal Amount { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - 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(NumericStringJsonConverter))] - public decimal Value { get; set; } - - [JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))] - 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/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 134ac4af5..c18c150e9 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -246,5 +246,5 @@ <_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" /> - + diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 9fb31470b..a1774bb10 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -269,50 +269,7 @@ namespace BTCPayServer.Controllers.GreenField PaymentMethods = entity.GetPaymentMethods().Select(method => method.GetId().ToString()).ToArray(), SpeedPolicy = entity.SpeedPolicy - }, - 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() - }; - }) + } }; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 5cc3d1688..1a376b35c 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -533,124 +533,11 @@ "additionalStatus": { "$ref": "#/components/schemas/InvoiceAdditionalStatus", "description": "a secondary status of the invoice" - }, - "paymentMethodData": { - "type": "object", - "nullable": false, - "additionalProperties": { - "$ref": "#/components/schemas/PaymentMethodDataModel" - }, - "description": "Activated payment methods details" } } } ] }, - "PaymentMethodDataModel": { - "type": "object", - "additionalProperties": false, - "properties": { - "destination": { - "type": "string", - "nullable": true, - "description": "The destination the payment must be made to" - }, - "paymentLink": { - "type": "string", - "nullable": true, - "description": "A payment link that helps pay to the payment destination" - }, - "rate": { - "type": "string", - "format": "decimal", - "description": "The rate between this payment method's currency and the invoice currency" - }, - "paymentMethodPaid": { - "type": "string", - "format": "decimal", - "description": "The amount paid by this payment method" - }, - "totalPaid": { - "type": "string", - "format": "decimal", - "description": "The total amount paid by all payment methods to the invoice, converted to this payment method's currency" - }, - "due": { - "type": "string", - "format": "decimal", - "description": "The total amount left to be paid, converted to this payment method's currency" - }, - "amount": { - "type": "string", - "format": "decimal", - "description": "The invoice amount, converted to this payment method's currency" - }, - "networkFee": { - "type": "string", - "format": "decimal", - "description": "The added merchant fee to pay for network costs of this payment method." - }, - "payments": { - "type": "array", - "nullable": true, - "items": { - "$ref": "#/components/schemas/Payment" - }, - "description": "Payments made with this payment method." - } - } - }, - "Payment": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "nullable": true, - "description": "A unique identifier for this payment" - }, - "receivedDate": { - "type": "string", - "format": "date-time", - "description": "The date the payment was recorded" - }, - "value": { - "type": "string", - "format": "decimal", - "description": "The value of the payment" - }, - "fee": { - "type": "string", - "format": "decimal", - "description": "The fee paid for the payment" - }, - "status": { - "$ref": "#/components/schemas/PaymentStatus", - "description": "The status of the payment" - }, - "destination": { - "type": "string", - "nullable": true, - "description": "The destination the payment was made to" - } - } - }, - "PaymentStatus": { - "type": "string", - "description": "", - "x-enumNames": [ - "Invalid", - "AwaitingConfirmation", - "AwaitingCompletion", - "Complete" - ], - "enum": [ - "Invalid", - "AwaitingConfirmation", - "AwaitingCompletion", - "Complete" - ] - }, "CreateInvoiceRequest": { "type": "object", "additionalProperties": false, @@ -699,8 +586,8 @@ "nullable": true, "items": { "type": "string" - }, - "description": "A specific set of payment methods to use for this invoice" + }, + "description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC_OnChain). By default, select all payment methods activated in the store." }, "expirationMinutes": { "type": "integer", From f2e94e681925df5170b3721ee18c88f261dabab2 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Aug 2020 21:24:37 +0900 Subject: [PATCH 14/16] Use nicer representation of payment methods in the Greenfield API --- .../AltcoinTests/AltcoinTests.cs | 2 +- BTCPayServer.Tests/UnitTest1.cs | 30 +++++++++++++++++++ .../GreenField/InvoiceController.cs | 2 +- BTCPayServer/Payments/PaymentMethodId.cs | 22 ++++++++++++-- BTCPayServer/Payments/PaymentTypes.Bitcoin.cs | 4 +++ .../Payments/PaymentTypes.Lightning.cs | 5 +++- BTCPayServer/Payments/PaymentTypes.cs | 18 ++++++++++- .../Monero/Payments/MoneroPaymentType.cs | 5 +++- .../swagger/v1/swagger.template.invoices.json | 2 +- 9 files changed, 82 insertions(+), 8 deletions(-) diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index 8d4cc2769..3f361a90f 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -877,7 +877,7 @@ normal: InvoiceEntity invoiceEntity = new InvoiceEntity(); invoiceEntity.Networks = networkProvider; invoiceEntity.Payments = new System.Collections.Generic.List(); - invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 }; + invoiceEntity.Price = 100; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } .SetPaymentMethodDetails( diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6b807c6ae..6788b6d00 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -151,6 +151,36 @@ namespace BTCPayServer.Tests } } + [Fact] + [Trait("Fast", "Fast")] + public void CanParsePaymentMethodId() + { + var id = PaymentMethodId.Parse("BTC"); + var id1 = PaymentMethodId.Parse("BTC-OnChain"); + var id2 = PaymentMethodId.Parse("BTC-BTCLike"); + Assert.Equal(id, id1); + Assert.Equal(id, id2); + Assert.Equal("BTC", id.ToString()); + Assert.Equal("BTC", id.ToString()); + id = PaymentMethodId.Parse("LTC"); + Assert.Equal("LTC", id.ToString()); + Assert.Equal("LTC", id.ToStringNormalized()); + id = PaymentMethodId.Parse("LTC-offchain"); + id1 = PaymentMethodId.Parse("LTC-OffChain"); + id2 = PaymentMethodId.Parse("LTC-LightningLike"); + Assert.Equal(id, id1); + Assert.Equal(id, id2); + Assert.Equal("LTC_LightningLike", id.ToString()); + Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized()); +#if ALTCOINS + id = PaymentMethodId.Parse("XMR"); + id1 = PaymentMethodId.Parse("XMR-MoneroLike"); + Assert.Equal(id, id1); + Assert.Equal("XMR_MoneroLike", id.ToString()); + Assert.Equal("XMR", id.ToStringNormalized()); +#endif + } + [Fact] [Trait("Fast", "Fast")] public async Task CheckNoDeadLink() diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index a1774bb10..1f589d09b 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -267,7 +267,7 @@ namespace BTCPayServer.Controllers.GreenField Monitoring = entity.MonitoringExpiration - entity.ExpirationTime, PaymentTolerance = entity.PaymentTolerance, PaymentMethods = - entity.GetPaymentMethods().Select(method => method.GetId().ToString()).ToArray(), + entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(), SpeedPolicy = entity.SpeedPolicy } }; diff --git a/BTCPayServer/Payments/PaymentMethodId.cs b/BTCPayServer/Payments/PaymentMethodId.cs index 77a047af5..37a902548 100644 --- a/BTCPayServer/Payments/PaymentMethodId.cs +++ b/BTCPayServer/Payments/PaymentMethodId.cs @@ -64,20 +64,38 @@ namespace BTCPayServer.Payments //BTCLike case is special because it is in legacy mode. return PaymentType == PaymentTypes.BTCLike ? CryptoCode : $"{CryptoCode}_{PaymentType}"; } + /// + /// A string we can expose to Greenfield API, not subjected to internal legacy + /// + /// + public string ToStringNormalized() + { + if (PaymentType == PaymentTypes.BTCLike) + return CryptoCode; +#if ALTCOINS + if (CryptoCode == "XMR" && PaymentType == PaymentTypes.MoneroLike) + return CryptoCode; +#endif + return $"{CryptoCode}-{PaymentType.ToStringNormalized()}"; + } public string ToPrettyString() { return $"{CryptoCode} ({PaymentType.ToPrettyString()})"; } - + static char[] Separators = new[] { '_', '-' }; public static bool TryParse(string str, out PaymentMethodId paymentMethodId) { str ??= ""; paymentMethodId = null; - var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries); + var parts = str.Split(Separators, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0 || parts.Length > 2) return false; PaymentType type = PaymentTypes.BTCLike; +#if ALTCOINS + if (parts[0].ToUpperInvariant() == "XMR") + type = PaymentTypes.MoneroLike; +#endif if (parts.Length == 2) { if (!PaymentTypes.TryParse(parts[1], out type)) diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index a6fe91f54..ffdbd7b12 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -19,6 +19,10 @@ namespace BTCPayServer.Payments public override string ToPrettyString() => "On-Chain"; public override string GetId() => "BTCLike"; + public override string ToStringNormalized() + { + return "OnChain"; + } public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 9236d39f7..999443d2c 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -17,7 +17,10 @@ namespace BTCPayServer.Payments public override string ToPrettyString() => "Off-Chain"; public override string GetId() => "LightningLike"; - + public override string ToStringNormalized() + { + return "LightningNetwork"; + } public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { return ((BTCPayNetwork)network)?.ToObject(str); diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index 391c79c30..6ca9c0cf2 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -22,6 +22,13 @@ namespace BTCPayServer.Payments /// public static LightningPaymentType LightningLike => LightningPaymentType.Instance; +#if ALTCOINS + /// + /// Monero payment + /// + public static MoneroPaymentType MoneroLike => MoneroPaymentType.Instance; +#endif + public static bool TryParse(string paymentType, out PaymentType type) { switch (paymentType.ToLowerInvariant()) @@ -36,7 +43,7 @@ namespace BTCPayServer.Payments break; #if ALTCOINS case "monerolike": - type = MoneroPaymentType.Instance; + type = PaymentTypes.MoneroLike; break; #endif default: @@ -61,6 +68,15 @@ namespace BTCPayServer.Payments return GetId(); } + /// + /// A string we can expose to Greenfield API, not subjected to internal legacy + /// + /// + public virtual string ToStringNormalized() + { + return ToString(); + } + public abstract string GetId(); public abstract CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str); public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData); diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs index 184d6c1d6..cdfe86260 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs @@ -14,7 +14,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public override string ToPrettyString() => "On-Chain"; public override string GetId() => "MoneroLike"; - + public override string ToStringNormalized() + { + return "Monero"; + } public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 1a376b35c..2cd9b467f 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -587,7 +587,7 @@ "items": { "type": "string" }, - "description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC_OnChain). By default, select all payment methods activated in the store." + "description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC-LightningNetwork). By default, select all payment methods activated in the store." }, "expirationMinutes": { "type": "integer", From f900d520da87c61125358f2136ddf087ead47307 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Aug 2020 21:31:08 +0900 Subject: [PATCH 15/16] Add InvoiceTime, Expiration and monitoring to Invoice greenfield API --- BTCPayServer.Client/Models/InvoiceData.cs | 6 ++++++ .../Controllers/GreenField/InvoiceController.cs | 3 +++ .../swagger/v1/swagger.template.invoices.json | 16 +++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 21209af29..f31644f94 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -13,5 +13,11 @@ namespace BTCPayServer.Client.Models public InvoiceStatus Status { get; set; } [JsonConverter(typeof(StringEnumConverter))] public InvoiceExceptionStatus AdditionalStatus { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset MonitoringExpiration { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset ExpirationTime { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset CreatedTime { get; set; } } } diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 1f589d09b..1840e0fbc 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -255,6 +255,9 @@ namespace BTCPayServer.Controllers.GreenField { return new InvoiceData() { + ExpirationTime = entity.ExpirationTime, + MonitoringExpiration = entity.MonitoringExpiration, + CreatedTime = entity.InvoiceTime, Amount = entity.Price, Id = entity.Id, Status = entity.Status, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 2cd9b467f..f020641a0 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -523,9 +523,23 @@ "properties": { "id": { "type": "string", - "nullable": true, "description": "The identifier of the invoice" }, + "createdTime": { + "type": "number", + "format": "int64", + "description": "The creation time of the invoice" + }, + "expirationTime": { + "type": "number", + "format": "int64", + "description": "The expiration time of the invoice" + }, + "monitoringTime": { + "type": "number", + "format": "int64", + "description": "The monitoring time of the invoice" + }, "status": { "$ref": "#/components/schemas/InvoiceStatus", "description": "The status of the invoice" From 709b06baa330fc670b2e4358dc69ecdb7b6aa4e5 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 27 Aug 2020 11:00:04 +0900 Subject: [PATCH 16/16] Remove invoices/email for this PR --- .../BTCPayServerClient.Invoices.cs | 11 --- BTCPayServer.Tests/GreenfieldAPITests.cs | 11 --- .../GreenField/InvoiceController.cs | 37 ---------- .../swagger/v1/swagger.template.invoices.json | 73 ------------------- 4 files changed, 132 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index d97f235db..7710ea118 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -47,17 +47,6 @@ namespace BTCPayServer.Client return await HandleResponse(response); } - public virtual async Task AddCustomerEmailToInvoice(string storeId, string invoiceId, - AddCustomerEmailRequest request, CancellationToken token = default) - { - if (request == null) - throw new ArgumentNullException(nameof(request)); - var response = await _httpClient.SendAsync( - CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/email", bodyPayload: request, - method: HttpMethod.Post), token); - return await HandleResponse(response); - } - public virtual async Task MarkInvoiceStatus(string storeId, string invoiceId, MarkInvoiceStatusRequest request, CancellationToken token = default) { diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index c17d83fe8..050a8a002 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -845,17 +845,6 @@ namespace BTCPayServer.Tests Assert.Equal(newInvoice.Metadata, invoice.Metadata); //update - await AssertHttpError(403, async () => - { - await viewOnly.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() - { - Email = "j@g.com" - }); - }); - await client.AddCustomerEmailToInvoice(user.StoreId, invoice.Id, new AddCustomerEmailRequest() - { - Email = "j@g.com" - }); invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () => diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 1840e0fbc..c80b4fe97 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -183,43 +183,6 @@ namespace BTCPayServer.Controllers.GreenField return await GetInvoice(storeId, invoiceId); } - [Authorize(Policy = Policies.CanCreateInvoice, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/email")] - public async Task AddCustomerEmail(string storeId, string invoiceId, - AddCustomerEmailRequest request) - { - var store = HttpContext.GetStoreData(); - if (store == null) - { - return NotFound(); - } - - var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); - if (invoice.StoreId != store.Id) - { - return NotFound(); - } - - if (!EmailValidator.IsEmail(request.Email)) - { - request.AddModelError(invoiceRequest => invoiceRequest.Email, "Invalid email address", - this); - } - else if (!string.IsNullOrEmpty(invoice.Metadata.BuyerEmail)) - { - request.AddModelError(invoiceRequest => invoiceRequest.Email, "Email address already set", - this); - } - - if (!ModelState.IsValid) - return this.CreateValidationError(ModelState); - - await _invoiceRepository.UpdateInvoice(invoice.Id, new UpdateCustomerModel() { Email = request.Email }); - - return await GetInvoice(storeId, invoiceId); - } - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")] diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index f020641a0..aee6d8cf9 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -216,79 +216,6 @@ ] } }, - "/api/v1/stores/{storeId}/invoices/{invoiceId}/email": { - "post": { - "tags": [ - "Invoices" - ], - "summary": "Add customer email to invoice", - "parameters": [ - { - "name": "storeId", - "in": "path", - "required": true, - "description": "The store to query", - "schema": { - "type": "string" - } - }, - { - "name": "invoiceId", - "in": "path", - "required": true, - "description": "The invoice to update", - "schema": { - "type": "string" - } - } - ], - "description": "Adds the customer's email to the invoice if it has not been set already.", - "operationId": "Invoices_AddCustomerEmail", - "responses": { - "200": { - "description": "The updated invoice", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceData" - } - } - } - }, - "400": { - "description": "A list of errors that occurred when updating the invoice", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden to update the invoice" - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddCustomerEmailRequest" - } - } - } - }, - "security": [ - { - "API Key": [ - "btcpay.store.cancreateinvoice" - ], - "Basic": [] - } - ] - } - }, "/api/v1/stores/{storeId}/invoices/{invoiceId}/status": { "post": { "tags": [
Item code@Model.ProductInformation.ItemCode@Model.TypedMetadata.ItemCode
Item Description@Model.ProductInformation.ItemDesc@Model.TypedMetadata.ItemDesc
Price