Create Metadata property for InvoiceEntity, migrate all data without logic there

This commit is contained in:
nicolas.dorier 2020-08-25 14:33:00 +09:00
parent 8dea7df82a
commit b2ff041ec0
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
22 changed files with 463 additions and 259 deletions

View file

@ -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

View file

@ -17,9 +17,6 @@
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);ALTCOINS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />

View file

@ -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<Client.Models.InvoiceData> 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<string>());
Assert.Equal("code", newInvoice.Metadata["itemCode"].Value<string>());
Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value<string>());
Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value<string>());
Assert.False(newInvoice.Metadata["physical"].Value<bool>());
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<Data.ApplicationDbContextFactory>();
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<string>());
}
}
[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<JsonSerializationException>(() =>
{
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
});Assert.Throws<JsonSerializationException>(() =>
{
jsonConverter.ReadJson(Get("null"), typeof(double), null, null);
});
Assert.Throws<JsonSerializationException>(() =>
{
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));

View file

@ -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;

View file

@ -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,

View file

@ -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<Buyer>(entity.Metadata);
pi = JsonConvert.DeserializeObject<ProductInformation>(entity.Metadata);
JObject.Parse(entity.Metadata).TryGetValue("orderid", StringComparison.InvariantCultureIgnoreCase,
out orderId);
}
catch
{
// ignored
}
invoiceMetadata = entity.Metadata.ToObject<InvoiceMetadata>();
}
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
};
}
}

View file

@ -29,7 +29,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] BitpayCreateInvoiceRequest invoice, CancellationToken cancellationToken)
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");

View file

@ -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,

View file

@ -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<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice,
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice,
StoreData store, string serverUrl, List<string> additionalTags = null,
CancellationToken cancellationToken = default)
{
@ -83,7 +81,7 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) {Facade = "pos/invoice"};
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> 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<CreateInvoiceRequest, BuyerInformation>(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<CreateInvoiceRequest, ProductInformation>(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, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(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;
}
}
}

View file

@ -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,

View file

@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers
DataWrapper<InvoiceResponse> invoice = null;
try
{
invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,

View file

@ -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<InvoiceEntity>(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)

View file

@ -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<string, int>();
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()

View file

@ -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<string> PaymentCurrencies { get; set; }
public JObject Metadata { get; set; }
}
}

View file

@ -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<Data.InvoiceEventData> Events { get; internal set; }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -56,9 +56,8 @@ namespace BTCPayServer.Services.Invoices.Export
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
{
var exportList = new List<ExportInvoiceHolder>();
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);

View file

@ -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<string, JToken> AdditionalData { get; set; }
[JsonProperty(PropertyName = "currency")]
public string Currency
public static InvoiceMetadata FromJObject(JObject jObject)
{
get; set;
return jObject.ToObject<InvoiceMetadata>(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<string, JToken> AdditionalData { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
@ -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<string, decimal>(),
PaymentTotals = new Dictionary<string, decimal>(),
@ -415,7 +509,7 @@ namespace BTCPayServer.Services.Invoices
var address = details?.GetPaymentDestination();
var exrates = new Dictionary<string, decimal>
{
{ 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, TDest>(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);
}
/// <summary>
/// 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.
/// </summary>
internal void MigrateLegacyInvoice()
{
T TryParseMetadata<T>(string field) where T : class
{
if (AdditionalData.TryGetValue(field, out var token) && token is JObject obj)
{
return obj.ToObject<T>();
}
return null;
}
if (TryParseMetadata<BuyerInformation>("buyerInformation") is BuyerInformation buyerInformation &&
TryParseMetadata<ProductInformation>("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<string>();
}
if (AdditionalData.TryGetValue("orderId", out var token2) &&
token2 is JValue val2 &&
val2.Type == JTokenType.String)
{
wellknown.OrderId = val2.Value<string>();
}
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<PaymentEntity, bool>((p) => true);
var paymentMethods = ParentEntity.GetPaymentMethods();
var totalDue = ParentEntity.ProductInformation.Price / Rate;
var totalDue = ParentEntity.Price / Rate;
var paid = 0m;
var cryptoPaid = 0.0m;

View file

@ -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;

View file

@ -1,4 +1,4 @@
@model InvoiceDetailsModel
@model InvoiceDetailsModel
@{
ViewData["Title"] = "Invoice " + Model.Id;
}
@ -88,7 +88,7 @@
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
<td>@Model.TypedMetadata.OrderId</td>
</tr>
<tr>
<th>Total fiat due</th>
@ -109,39 +109,39 @@
<table class="table table-sm table-responsive-md removetopborder">
<tr>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
<td>@Model.TypedMetadata.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
<td><a href="mailto:@Model.TypedMetadata.BuyerEmail">@Model.TypedMetadata.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
<td>@Model.TypedMetadata.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
<td>@Model.TypedMetadata.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
<td>@Model.TypedMetadata.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
<td>@Model.TypedMetadata.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
<td>@Model.TypedMetadata.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
<td>@Model.TypedMetadata.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
<td>@Model.TypedMetadata.BuyerZip</td>
</tr>
</table>
@if (Model.PosData.Count == 0)
@ -150,11 +150,11 @@
<table class="table table-sm table-responsive-md removetopborder">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
<tr>
<th>Price</th>
@ -177,11 +177,11 @@
<table class="table table-sm table-responsive-md removetopborder">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
<tr>
<th>Price</th>