mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Remove JSON in strings from JObjects (#4703)
This commit is contained in:
parent
e89b1826ce
commit
c229425534
@ -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()
|
||||
{
|
||||
|
@ -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)]
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
71
BTCPayServer/IHasAdditionalData.cs
Normal file
71
BTCPayServer/IHasAdditionalData.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 ||
|
||||
|
Loading…
Reference in New Issue
Block a user