Remove JSON in strings from JObjects (#4703)

This commit is contained in:
Nicolas Dorier 2023-02-25 23:34:49 +09:00 committed by GitHub
parent e89b1826ce
commit c229425534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 363 additions and 241 deletions

View File

@ -1218,21 +1218,14 @@ namespace BTCPayServer.Tests
{(null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>() {{string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>() {{string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>() {{"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})},
{
("{ invalidjson file here}",
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
},
// Duplicate keys should not crash things
{("{ \"key\": true, \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
};
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(tuple.input));
Assert.Equal(tuple.expectedOutput, UIInvoiceController.PosDataParser.ParsePosData(string.IsNullOrEmpty(tuple.input) ? null : JToken.Parse(tuple.input)));
});
}
[Fact]
@ -1806,6 +1799,70 @@ namespace BTCPayServer.Tests
}
}
}
[Fact]
public void CanParseMetadata()
{
var metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": {\"test\":\"a\"}}"));
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosDataLegacy);
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosData.ToString());
// Legacy, as string
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": \"{\\\"test\\\":\\\"a\\\"}\"}"));
Assert.Equal("{\"test\":\"a\"}", metadata.PosDataLegacy);
Assert.Equal(JObject.Parse("{\"test\":\"a\"}").ToString(), metadata.PosData.ToString());
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": \"nobject\"}"));
Assert.Equal("nobject", metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{\"posData\": null}"));
Assert.Null(metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
metadata = InvoiceMetadata.FromJObject(JObject.Parse("{}"));
Assert.Null(metadata.PosDataLegacy);
Assert.Null(metadata.PosData);
}
[Fact]
public void CanParseInvoiceEntityDerivationStrategies()
{
// We have 3 ways of serializing the derivation strategies:
// through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject
// Let's check that InvoiceEntity is similar in all cases.
var legacy = new JObject()
{
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", new BTCPayNetworkProvider(ChainName.Regtest).BTC);
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
var legacy2 = new JObject()
{
["derivationStrategies"] = scheme.ToJson()
};
var newformat = new JObject()
{
["derivationStrategies"] = JObject.Parse(scheme.ToJson())
};
//new BTCPayNetworkProvider(ChainName.Regtest)
#pragma warning disable CS0618 // Type or member is obsolete
var formats = new[] { legacy, legacy2, newformat }
.Select(o =>
{
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(o.ToString());
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
return entity.DerivationStrategies.ToString();
})
.ToHashSet();
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal(1, formats.Count);
}
[Fact]
public void PaymentMethodIdConverterIsGraceful()
{

View File

@ -1695,37 +1695,17 @@ namespace BTCPayServer.Tests
var testCases =
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{(null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{
("non-json-content",
new Dictionary<string, object>() {{string.Empty, "non-json-content"}})
},
{("[1,2,3]", new Dictionary<string, object>() {{string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>() {{"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})},
{
("{ invalidjson file here}",
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
}
{("{ \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
};
var tasks = new List<Task>();
foreach (var valueTuple in testCases)
{
tasks.Add(user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC") { PosData = valueTuple.input })
.ContinueWith(async task =>
{
var result = await controller.Invoice(task.Result.Id);
var viewModel =
Assert.IsType<InvoiceDetailsModel>(
Assert.IsType<ViewResult>(result).Model);
Assert.Equal(valueTuple.expectedOutput, viewModel.PosData);
}));
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC") { PosData = valueTuple.input });
var result = await controller.Invoice(invoice.Id);
var viewModel = result.AssertViewModel<InvoiceDetailsModel>();
Assert.Equal(valueTuple.expectedOutput, viewModel.AdditionalData["posData"]);
}
await Task.WhenAll(tasks);
}
[Fact(Timeout = LongRunningTestTimeout)]

View File

@ -40,6 +40,17 @@ namespace BTCPayServer.Controllers
{
public partial class UIInvoiceController
{
static UIInvoiceController()
{
InvoiceAdditionalDataExclude =
typeof(InvoiceMetadata)
.GetProperties()
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
InvoiceAdditionalDataExclude.Remove(nameof(InvoiceMetadata.PosData));
}
static readonly HashSet<string> InvoiceAdditionalDataExclude;
[HttpGet("invoices/{invoiceId}/deliveries/{deliveryId}/request")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> WebhookDelivery(string invoiceId, string deliveryId)
@ -106,13 +117,9 @@ namespace BTCPayServer.Controllers
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions);
var invoiceState = invoice.GetInvoiceState();
var posData = PosDataParser.ParsePosData(invoice.Metadata.PosData);
var metaData = PosDataParser.ParsePosData(invoice.Metadata.ToJObject().ToString());
var excludes = typeof(InvoiceMetadata).GetProperties()
.Select(p => char.ToLowerInvariant(p.Name[0]) + p.Name[1..])
.ToList();
var metaData = PosDataParser.ParsePosData(invoice.Metadata.ToJObject());
var additionalData = metaData
.Where(dict => !excludes.Contains(dict.Key))
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
.ToDictionary(dict=> dict.Key, dict=> dict.Value);
var model = new InvoiceDetailsModel
{
@ -139,7 +146,6 @@ namespace BTCPayServer.Controllers
TypedMetadata = invoice.Metadata,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events,
PosData = posData,
Metadata = metaData,
AdditionalData = additionalData,
Archived = invoice.Archived,
@ -236,9 +242,7 @@ namespace BTCPayServer.Controllers
vm.Amount = payments.Sum(p => p!.Paid);
vm.Payments = receipt.ShowPayments is false ? null : payments;
vm.AdditionalData = receiptData is null
? new Dictionary<string, object>()
: PosDataParser.ParsePosData(receiptData.ToString());
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
return View(vm);
}
@ -1239,30 +1243,20 @@ namespace BTCPayServer.Controllers
public class PosDataParser
{
public static Dictionary<string, object> ParsePosData(string posData)
public static Dictionary<string, object> ParsePosData(JToken? posData)
{
var result = new Dictionary<string, object>();
if (string.IsNullOrEmpty(posData))
if (posData is JObject jobj)
{
return result;
}
try
{
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
foreach (var item in jobj)
{
ParsePosDataItem(item, ref result);
}
}
catch
{
result.TryAdd(string.Empty, posData);
}
return result;
}
public static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
{
switch (item.Value?.Type)
{
@ -1270,12 +1264,12 @@ namespace BTCPayServer.Controllers
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i]));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
result.TryAdd(item.Key, ParsePosData(item.Value));
break;
case null:
break;

View File

@ -30,6 +30,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
@ -117,7 +118,7 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "The expirationTime is set too soon");
}
entity.Metadata.OrderId = invoice.OrderId;
entity.Metadata.PosData = invoice.PosData;
entity.Metadata.PosDataLegacy = invoice.PosData;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public static class HasAdditionalDataExtensions
{
public static T GetAdditionalData<T>(this IHasAdditionalData o, string propName)
{
if (o.AdditionalData == null || !(o.AdditionalData.TryGetValue(propName, out var jt) is true))
return default;
if (jt.Type == JTokenType.Null)
return default;
if (typeof(T) == typeof(string))
{
return (T)(object)jt.ToString();
}
try
{
return jt.Value<T>();
}
catch (Exception)
{
return default;
}
}
public static void SetAdditionalData<T>(this IHasAdditionalData o, string propName, T value)
{
JToken data;
if (typeof(T) == typeof(string) && value is string v)
{
data = new JValue(v);
o.AdditionalData ??= new Dictionary<string, JToken>();
o.AdditionalData.AddOrReplace(propName, data);
return;
}
if (value is null)
{
o.AdditionalData?.Remove(propName);
}
else
{
try
{
if (value is string s)
{
data = JToken.Parse(s);
}
else
{
data = JToken.FromObject(value);
}
}
catch (Exception)
{
data = JToken.FromObject(value);
}
o.AdditionalData ??= new Dictionary<string, JToken>();
o.AdditionalData.AddOrReplace(propName, data);
}
}
}
public interface IHasAdditionalData
{
IDictionary<string, JToken> AdditionalData { get; set; }
}
}

View File

@ -124,7 +124,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, object> PosData { get; set; }
public Dictionary<string, object> Metadata { get; set; }
public Dictionary<string, object> AdditionalData { get; set; }
public List<PaymentEntity> Payments { get; set; }

View File

@ -151,6 +151,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
}
var jposData = TryParseJObject(posData);
string title;
decimal? price;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
@ -191,10 +192,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) && currentView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
if (currentView == PosViewType.Cart &&
AppService.TryParsePosCartItems(jposData, out var cartItems))
{
var choices = _appService.GetPOSItems(settings.Template, settings.Currency);
var expectedMinimumAmount = 0m;
@ -257,7 +257,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return View("PostRedirect", vm);
}
formResponseJObject = JObject.Parse(formResponse);
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
var form = Form.Parse(formData.Config);
form.SetValues(formResponseJObject);
if (!FormDataService.Validate(form, ModelState))
@ -269,7 +269,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
formResponseJObject = form.GetValues();
break;
}
try
{
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
@ -287,7 +286,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
: Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
@ -298,23 +296,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
cancellationToken, entity =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
entity.Metadata.PosData = jposData;
if (formResponseJObject is null) return;
var meta = entity.Metadata.ToJObject();
if (formResponseJObject.ContainsKey("posData") && meta.TryGetValue("posData", out var posDataValue) && posDataValue.Type == JTokenType.String)
{
try
{
meta["posData"] = JObject.Parse(posDataValue.Value<string>());
}
catch (Exception e)
{
// ignored as we don't want to break the invoice creation
}
}
formResponseJObject.Merge(meta);
entity.Metadata = InvoiceMetadata.FromJObject(formResponseJObject);
meta.Merge(formResponseJObject);
entity.Metadata = InvoiceMetadata.FromJObject(meta);
});
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
@ -330,6 +316,18 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
}
}
private JObject TryParseJObject(string posData)
{
try
{
return JObject.Parse(posData);
}
catch
{
}
return null;
}
[HttpPost("/apps/{appId}/pos/form/{viewType?}")]
public async Task<IActionResult> POSForm(string appId, PosViewType? viewType = null)
{

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@ -18,6 +19,7 @@ using Ganss.XSS;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YamlDotNet.Core;
@ -250,7 +252,7 @@ namespace BTCPayServer.Services.Apps
var itemCount = paidInvoices
.Where(entity => entity.Currency.Equals(settings.Currency, StringComparison.OrdinalIgnoreCase) && (
// The POS data is present for the cart view, where multiple items can be bought
!string.IsNullOrEmpty(entity.Metadata.PosData) ||
entity.Metadata.PosData != null ||
// The item code should be present for all types other than the cart and keypad
!string.IsNullOrEmpty(entity.Metadata.ItemCode)
))
@ -335,10 +337,10 @@ namespace BTCPayServer.Services.Apps
{
return (res, e) =>
{
if (!string.IsNullOrEmpty(e.Metadata.PosData))
if (e.Metadata.PosData != null)
{
// flatten single items from POS data
var data = JsonConvert.DeserializeObject<PosAppData>(e.Metadata.PosData);
var data = e.Metadata.PosData.ToObject<PosAppData>();
if (data is not { Cart.Length: > 0 })
return res;
foreach (var lineItem in data.Cart)
@ -777,18 +779,33 @@ namespace BTCPayServer.Services.Apps
return false;
}
}
public static bool TryParsePosCartItems(string posData, out Dictionary<string, int> cartItems)
#nullable enable
public static bool TryParsePosCartItems(JObject? posData, [MaybeNullWhen(false)] out Dictionary<string, int> cartItems)
{
cartItems = null;
if (!TryParseJson(posData, out var posDataObj) ||
!posDataObj.TryGetValue("cart", out var cartObject))
if (posData is null)
return false;
cartItems = cartObject.Select(token => (JObject)token)
.ToDictionary(o => o.GetValue("id", StringComparison.InvariantCulture)?.ToString(),
o => int.Parse(o.GetValue("count", StringComparison.InvariantCulture)?.ToString() ?? string.Empty, CultureInfo.InvariantCulture));
if (!posData.TryGetValue("cart", out var cartObject))
return false;
if (cartObject is null)
return false;
cartItems = new();
foreach (var o in cartObject.OfType<JObject>())
{
var id = o.GetValue("id", StringComparison.InvariantCulture)?.ToString();
if (id != null)
{
var countStr = o.GetValue("count", StringComparison.InvariantCulture)?.ToString() ?? string.Empty;
if (int.TryParse(countStr, out var count))
{
cartItems.TryAdd(id, count);
}
}
}
return true;
}
#nullable restore
}
public class ItemStats

View File

@ -30,7 +30,7 @@ namespace BTCPayServer.Services.Invoices
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
}
}
public class InvoiceMetadata
public class InvoiceMetadata : IHasAdditionalData
{
public static readonly JsonSerializer MetadataSerializer;
static InvoiceMetadata()
@ -45,165 +45,167 @@ namespace BTCPayServer.Services.Invoices
[JsonIgnore]
public string OrderId
{
get => GetMetadata<string>("orderId");
set => SetMetadata("orderId", value);
get => this.GetAdditionalData<string>("orderId");
set => this.SetAdditionalData("orderId", value);
}
[JsonIgnore]
public string OrderUrl
{
get => GetMetadata<string>("orderUrl");
set => SetMetadata("orderUrl", value);
get => this.GetAdditionalData<string>("orderUrl");
set => this.SetAdditionalData("orderUrl", value);
}
[JsonIgnore]
public string PaymentRequestId
{
get => GetMetadata<string>("paymentRequestId");
set => SetMetadata("paymentRequestId", value);
get => this.GetAdditionalData<string>("paymentRequestId");
set => this.SetAdditionalData("paymentRequestId", value);
}
[JsonIgnore]
public string BuyerName
{
get => GetMetadata<string>("buyerName");
set => SetMetadata("buyerName", value);
get => this.GetAdditionalData<string>("buyerName");
set => this.SetAdditionalData("buyerName", value);
}
[JsonIgnore]
public string BuyerEmail
{
get => GetMetadata<string>("buyerEmail");
set => SetMetadata("buyerEmail", value);
get => this.GetAdditionalData<string>("buyerEmail");
set => this.SetAdditionalData("buyerEmail", value);
}
[JsonIgnore]
public string BuyerCountry
{
get => GetMetadata<string>("buyerCountry");
set => SetMetadata("buyerCountry", value);
get => this.GetAdditionalData<string>("buyerCountry");
set => this.SetAdditionalData("buyerCountry", value);
}
[JsonIgnore]
public string BuyerZip
{
get => GetMetadata<string>("buyerZip");
set => SetMetadata("buyerZip", value);
get => this.GetAdditionalData<string>("buyerZip");
set => this.SetAdditionalData("buyerZip", value);
}
[JsonIgnore]
public string BuyerState
{
get => GetMetadata<string>("buyerState");
set => SetMetadata("buyerState", value);
get => this.GetAdditionalData<string>("buyerState");
set => this.SetAdditionalData("buyerState", value);
}
[JsonIgnore]
public string BuyerCity
{
get => GetMetadata<string>("buyerCity");
set => SetMetadata("buyerCity", value);
get => this.GetAdditionalData<string>("buyerCity");
set => this.SetAdditionalData("buyerCity", value);
}
[JsonIgnore]
public string BuyerAddress2
{
get => GetMetadata<string>("buyerAddress2");
set => SetMetadata("buyerAddress2", value);
get => this.GetAdditionalData<string>("buyerAddress2");
set => this.SetAdditionalData("buyerAddress2", value);
}
[JsonIgnore]
public string BuyerAddress1
{
get => GetMetadata<string>("buyerAddress1");
set => SetMetadata("buyerAddress1", value);
get => this.GetAdditionalData<string>("buyerAddress1");
set => this.SetAdditionalData("buyerAddress1", value);
}
[JsonIgnore]
public string BuyerPhone
{
get => GetMetadata<string>("buyerPhone");
set => SetMetadata("buyerPhone", value);
get => this.GetAdditionalData<string>("buyerPhone");
set => this.SetAdditionalData("buyerPhone", value);
}
[JsonIgnore]
public string ItemDesc
{
get => GetMetadata<string>("itemDesc");
set => SetMetadata("itemDesc", value);
get => this.GetAdditionalData<string>("itemDesc");
set => this.SetAdditionalData("itemDesc", value);
}
[JsonIgnore]
public string ItemCode
{
get => GetMetadata<string>("itemCode");
set => SetMetadata("itemCode", value);
get => this.GetAdditionalData<string>("itemCode");
set => this.SetAdditionalData("itemCode", value);
}
[JsonIgnore]
public bool? Physical
{
get => GetMetadata<bool?>("physical");
set => SetMetadata("physical", value);
get => this.GetAdditionalData<bool?>("physical");
set => this.SetAdditionalData("physical", value);
}
[JsonIgnore]
public decimal? TaxIncluded
{
get => GetMetadata<decimal?>("taxIncluded");
set => SetMetadata("taxIncluded", value);
get => this.GetAdditionalData<decimal?>("taxIncluded");
set => this.SetAdditionalData("taxIncluded", value);
}
/// <summary>
/// posData is a field that may be treated differently for presentation and in some legacy API
/// Before, it was a string field which could contain some JSON data inside.
/// For making it easier to query on the DB, and for logic using PosData in the code, we decided to
/// parse it as a JObject.
///
/// This property will return the posData as a JObject, even if it's a Json string inside.
/// </summary>
[JsonIgnore]
public string PosData
public JObject PosData
{
get => GetMetadata<string>("posData");
set => SetMetadata("posData", value);
get
{
if (AdditionalData == null || !(AdditionalData.TryGetValue("posData", out var jt) is true))
return default;
if (jt.Type == JTokenType.Null)
return default;
if (jt.Type == JTokenType.String)
try
{
return JObject.Parse(jt.Value<string>());
}
catch
{
return null;
}
if (jt.Type == JTokenType.Object)
return (JObject)jt;
return null;
}
set
{
this.SetAdditionalData<JObject>("posData", value);
}
}
/// <summary>
/// See comments on <see cref="PosData"/>
/// </summary>
[JsonIgnore]
public string PosDataLegacy
{
get
{
return this.GetAdditionalData<string>("posData");
}
set
{
if (value != null)
{
try
{
PosData = JObject.Parse(value);
return;
}
catch
{
}
}
this.SetAdditionalData<string>("posData", value);
}
}
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
public T GetMetadata<T>(string propName)
{
if (AdditionalData == null || !(AdditionalData.TryGetValue(propName, out var jt) is true))
return default;
if (jt.Type == JTokenType.Null)
return default;
if (typeof(T) == typeof(string))
{
return (T)(object)jt.ToString();
}
try
{
return jt.Value<T>();
}
catch (Exception)
{
return default;
}
}
public void SetMetadata<T>(string propName, T value)
{
JToken data;
if (typeof(T) == typeof(string) && value is string v)
{
data = new JValue(v);
AdditionalData ??= new Dictionary<string, JToken>();
AdditionalData.AddOrReplace(propName, data);
return;
}
if (value is null)
{
AdditionalData?.Remove(propName);
}
else
{
try
{
if (value is string s)
{
data = JToken.Parse(s);
}
else
{
data = JToken.FromObject(value);
}
}
catch (Exception)
{
data = JToken.FromObject(value);
}
AdditionalData ??= new Dictionary<string, JToken>();
AdditionalData.AddOrReplace(propName, data);
}
}
public static InvoiceMetadata FromJObject(JObject jObject)
{
return jObject.ToObject<InvoiceMetadata>(MetadataSerializer);
@ -214,7 +216,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public class InvoiceEntity
public class InvoiceEntity : IHasAdditionalData
{
class BuyerInformation
{
@ -302,11 +304,35 @@ namespace BTCPayServer.Services.Invoices
.Select(t => t.Substring(prefix.Length)).ToArray();
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy { get; set; }
[Obsolete("Use GetPaymentMethodFactories() instead")]
public string DerivationStrategies { get; set; }
[JsonIgnore]
public JObject DerivationStrategies
{
get
{
if (AdditionalData is null || AdditionalData.TryGetValue("derivationStrategies", out var v) is not true)
{
if (AdditionalData is null || AdditionalData.TryGetValue("derivationStrategy", out v) is not true || Networks.BTC is null)
return null;
// This code is very unlikely called. "derivationStrategy" is an old property that was present in 2018.
// And this property is only read for unexpired invoices with lazy payments (Feature unavailable then)
var settings = DerivationSchemeSettings.Parse(v.ToString(), Networks.BTC);
settings.AccountOriginal = v.ToString();
settings.Source = "ManualDerivationScheme";
return JObject.Parse(settings.ToJson());
}
if (v.Type == JTokenType.String)
return JObject.Parse(v.Value<string>());
if (v.Type == JTokenType.Object)
return (JObject)v;
return null;
}
set
{
this.SetAdditionalData("derivationStrategies", value);
this.SetAdditionalData<string>("derivationStrategy", null);
}
}
public IEnumerable<T> GetSupportedPaymentMethod<T>(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod
{
return
@ -321,11 +347,9 @@ namespace BTCPayServer.Services.Invoices
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethod()
{
#pragma warning disable CS0618
bool btcReturned = false;
if (!string.IsNullOrEmpty(DerivationStrategies))
if (DerivationStrategies != null)
{
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
foreach (var strat in DerivationStrategies.Properties())
{
if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId))
{
@ -334,20 +358,10 @@ namespace BTCPayServer.Services.Invoices
var network = Networks.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == Networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike)
btcReturned = true;
yield return paymentMethodId.PaymentType.DeserializeSupportedPaymentMethod(network, strat.Value);
}
}
}
if (!btcReturned && !string.IsNullOrEmpty(DerivationStrategy))
{
if (Networks.BTC != null)
{
yield return BTCPayServer.DerivationSchemeSettings.Parse(DerivationStrategy, Networks.BTC);
}
}
#pragma warning restore CS0618
}
@ -358,10 +372,8 @@ namespace BTCPayServer.Services.Invoices
{
obj.Add(strat.PaymentId.ToString(), PaymentMethodExtensions.Serialize(strat));
#pragma warning disable CS0618
// This field should eventually disappear
DerivationStrategy = null;
}
DerivationStrategies = JsonConvert.SerializeObject(obj);
DerivationStrategies = obj;
#pragma warning restore CS0618
}
@ -463,7 +475,7 @@ namespace BTCPayServer.Services.Invoices
Id = Id,
StoreId = StoreId,
OrderId = Metadata.OrderId,
PosData = Metadata.PosData,
PosData = Metadata.PosDataLegacy,
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime,
@ -710,7 +722,7 @@ namespace BTCPayServer.Services.Invoices
token is JValue val &&
val.Type == JTokenType.String)
{
wellknown.PosData = val.Value<string>();
wellknown.PosDataLegacy = val.Value<string>();
}
if (AdditionalData.TryGetValue("orderId", out var token2) &&
token2 is JValue val2 &&
@ -997,7 +1009,7 @@ namespace BTCPayServer.Services.Invoices
NextNetworkFee = NextNetworkFee
};
}
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString());
switch (details)
{

View File

@ -253,39 +253,32 @@
</table>
</div>
<div class="d-flex flex-column gap-5">
@if (Model.PosData.Any())
{
<div>
<h3 class="mb-3">Product Information</h3>
<table class="table mb-0">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
{
<tr>
<th class="fw-semibold">Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th class="fw-semibold">Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
<div>
<h3 class="mb-3">Product Information</h3>
<table class="table mb-0">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
{
<tr>
<th class="fw-semibold">Price</th>
<td>@Model.Fiat</td>
<th class="fw-semibold">Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
@if (Model.TaxIncluded is not null)
{
<tr>
<th class="fw-semibold">Tax Included</th>
<td>@Model.TaxIncluded</td>
</tr>
}
</table>
</div>
}
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
{
<tr>
<th class="fw-semibold">Item Description</th>
<td>@Model.TypedMetadata.ItemDesc</td>
</tr>
}
@if (Model.TaxIncluded is not null)
{
<tr>
<th class="fw-semibold">Tax Included</th>
<td>@Model.TaxIncluded</td>
</tr>
}
</table>
</div>
@if (Model.TypedMetadata.BuyerName is not null ||
Model.TypedMetadata.BuyerEmail is not null ||
Model.TypedMetadata.BuyerPhone is not null ||