Compare commits

...

1 Commits

15 changed files with 170 additions and 130 deletions

View File

@ -240,7 +240,7 @@ namespace BTCPayServer.Tests
Assert.True(valid);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task EnsureSwaggerPermissionsDocumented()
@ -252,7 +252,7 @@ namespace BTCPayServer.Tests
var description =
"BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n#OTHERPERMISSIONS#\n\nThe following permissions are available if the user is an administrator:\n\n#SERVERPERMISSIONS#\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n#STOREPERMISSIONS#\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n";
var storePolicies =
ManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
Policies.IsStorePolicy(pair.Key) && !pair.Key.EndsWith(":", StringComparison.InvariantCulture));
@ -270,7 +270,7 @@ namespace BTCPayServer.Tests
.Replace("#STOREPERMISSIONS#",
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")));
Logs.Tester.LogInformation(description);
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<HomeController>(acc.UserId, acc.StoreId)
.Swagger()).Value.ToJson();
@ -668,7 +668,7 @@ namespace BTCPayServer.Tests
AssertPayoutLabel(info);
}
[Fact]
[Trait("Fast", "Fast")]
@ -905,7 +905,7 @@ namespace BTCPayServer.Tests
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m));
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance );
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance);
await tester.ExplorerNode.GenerateAsync(1);
await Task.Delay(100); // wait a bit for payment to process before fetching new invoice
var newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
@ -2321,6 +2321,8 @@ namespace BTCPayServer.Tests
{("[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"}})},
// Duplicate keys should not crash things
{("{ \"key\": true, \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})},
{
("{ invalidjson file here}",
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
@ -2330,6 +2332,17 @@ namespace BTCPayServer.Tests
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, InvoiceController.PosDataParser.ParsePosData(tuple.input));
JObject jobj = null;
try
{
jobj = JObject.Parse(tuple.input);
}
catch
{
}
// If the posData is directly a jobject, it should parse the same as if it was a string with json encoded
if (jobj != null)
Assert.Equal(tuple.expectedOutput, InvoiceController.PosDataParser.ParsePosData(jobj));
});
}
@ -3129,7 +3142,7 @@ namespace BTCPayServer.Tests
("\\test.com", false),
("te\\st.com", false)
};
foreach(var t in tests)
foreach (var t in tests)
{
Assert.Equal(t.Item2, t.Item1.IsValidFileName());
}
@ -3439,7 +3452,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
await acc.CreateStoreAsync();
// Test if legacy DerivationStrategy column is converted to DerivationStrategies
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav";
@ -3454,7 +3467,7 @@ namespace BTCPayServer.Tests
Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString());
Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString());
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
@ -3601,43 +3614,43 @@ namespace BTCPayServer.Tests
var settings = tester.PayTester.GetService<SettingsRepository>();
var emailSenderFactory = tester.PayTester.GetService<EmailSenderFactory>();
Assert.Null(await Assert.IsType<ServerEmailSender>(emailSenderFactory.GetEmailSender()).GetEmailSettings());
Assert.Null(await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = false });
await settings.UpdateSetting(new EmailSettings()
{
From = "admin@admin.com",
Login = "admin@admin.com",
Password = "admin@admin.com",
Port = 1234,
Server = "admin.com",
EnableSSL = true
From = "admin@admin.com",
Login = "admin@admin.com",
Password = "admin@admin.com",
Port = 1234,
Server = "admin.com",
EnableSSL = true
});
Assert.Equal("admin@admin.com",(await Assert.IsType<ServerEmailSender>(emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Equal("admin@admin.com",(await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Equal("admin@admin.com", (await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = true });
Assert.Equal("admin@admin.com",(await Assert.IsType<ServerEmailSender>(emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Null(await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
Assert.IsType<RedirectToActionResult>(await acc.GetController<StoresController>().Emails(acc.StoreId, new EmailsViewModel(new EmailSettings()
{
From = "store@store.com",
From = "store@store.com",
Login = "store@store.com",
Password = "store@store.com",
Port = 1234,
Server = "store.com",
EnableSSL = true
}), ""));
Assert.Equal("store@store.com",(await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
Assert.Equal("store@store.com", (await Assert.IsType<StoreEmailSender>(emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
}
}
}
}

View File

@ -120,7 +120,7 @@ namespace BTCPayServer.Controllers
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded.AsDecimal() ?? 0.0m, invoice.Currency),
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
TypedMetadata = invoice.Metadata,
@ -539,7 +539,7 @@ namespace BTCPayServer.Controllers
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
OrderId = invoice.Metadata.OrderId,
OrderId = invoice.Metadata.OrderId.AsString(),
InvoiceId = invoice.Id,
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
CustomCSSLink = storeBlob.CustomCSS,
@ -556,7 +556,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.Metadata.ItemDesc,
ItemDesc = invoice.Metadata.ItemDesc.AsString(),
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
RedirectAutomatically = invoice.RedirectAutomatically,
@ -725,7 +725,7 @@ namespace BTCPayServer.Controllers
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
OrderId = invoice.Metadata.OrderId.AsString() ?? string.Empty,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
@ -900,42 +900,55 @@ 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 null)
{
return result;
}
try
JObject posDataJObj = posData as JObject;
if (posDataJObj is null)
{
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
if (posData.AsString() is string posDataStr)
{
switch (item.Value.Type)
if (string.IsNullOrEmpty(posDataStr))
return result;
try
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());
break;
posDataJObj = JObject.Parse(posDataStr);
}
catch
{
result.TryAdd(string.Empty, posDataStr);
}
}
}
catch
if (posDataJObj is null)
return result;
foreach (var item in posDataJObj)
{
result.Add(string.Empty, posData);
switch (item.Value.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i]));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value));
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
}
return result;
}

View File

@ -196,9 +196,9 @@ namespace BTCPayServer.Controllers
if (entity.Metadata.BuyerEmail != null)
{
if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail))
if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail.AsString()))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.Metadata.BuyerEmail;
entity.RefundMail = entity.Metadata.BuyerEmail.AsString();
}
entity.Status = InvoiceStatusLegacy.New;
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();

View File

@ -29,7 +29,10 @@ namespace BTCPayServer.Controllers
var usersQuery = _UserManager.Users;
if (!string.IsNullOrWhiteSpace(model.SearchTerm))
{
#pragma warning disable CA1307 // Specify StringComparison
// This is Entity query, can't pass string comparison without crashing
usersQuery = usersQuery.Where(u => u.Email.Contains(model.SearchTerm));
#pragma warning restore CA1307 // Specify StringComparison
}
if (sortOrder != null)

View File

@ -34,6 +34,7 @@ using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using System.Numerics;
namespace BTCPayServer
{
@ -427,6 +428,42 @@ namespace BTCPayServer
{
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
}
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JToken.cs#L1235
private static readonly JTokenType[] StringTypes = new[] { JTokenType.Date, JTokenType.Integer, JTokenType.Float, JTokenType.String, JTokenType.Comment, JTokenType.Raw, JTokenType.Boolean, JTokenType.Bytes, JTokenType.Guid, JTokenType.TimeSpan, JTokenType.Uri };
public static string AsString(this JToken tok)
{
if (tok is JValue v && StringTypes.Contains(v.Type))
{
if (v.Value is null)
return null;
if (v.Value is byte[] bytes)
return Convert.ToBase64String(bytes);
if (v.Value is BigInteger bigint)
return bigint.ToString(CultureInfo.InvariantCulture);
return Convert.ToString(v.Value, CultureInfo.InvariantCulture);
}
return null;
}
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JToken.cs#L1108
private static readonly JTokenType[] NumberTypes = new[] { JTokenType.Integer, JTokenType.Float, JTokenType.String, JTokenType.Comment, JTokenType.Raw, JTokenType.Boolean };
public static decimal? AsDecimal(this JToken tok)
{
if (tok is JValue v && NumberTypes.Contains(v.Type))
{
if (v.Value is null)
return null;
if (v.Value is BigInteger bigint)
return (decimal)bigint;
return Convert.ToDecimal(v.Value, CultureInfo.InvariantCulture);
}
return null;
}
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
{
ctx.Items["BTCPAY.STOREDATA"] = storeData;

View File

@ -35,7 +35,7 @@ namespace BTCPayServer
{
var negative = sats < 0;
var amt = sats.ToString(CultureInfo.InvariantCulture)
.Replace("-", "")
.Replace("-", "", StringComparison.OrdinalIgnoreCase)
.PadLeft(divisibility, '0');
amt = amt.Length == divisibility ? $"0.{amt}" : amt.Insert(amt.Length - divisibility, ".");
return decimal.Parse($"{(negative? "-": string.Empty)}{amt}", CultureInfo.InvariantCulture);

View File

@ -104,8 +104,8 @@ namespace BTCPayServer.HostedServices
return;
}
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode.AsString()) ||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData.AsString(), out cartItems)))
{
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
@ -115,9 +115,9 @@ namespace BTCPayServer.HostedServices
}
var items = cartItems ?? new Dictionary<string, int>();
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode.AsString()))
{
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode.AsString(), 1);
}
_eventAggregator.Publish(new UpdateAppInventory()

View File

@ -89,9 +89,9 @@ namespace BTCPayServer.HostedServices
textSearch.Add(invoice.Id);
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.Metadata.OrderId);
textSearch.Add(invoice.Metadata.OrderId.AsString());
textSearch.Add(invoice.StoreId);
textSearch.Add(invoice.Metadata.BuyerEmail);
textSearch.Add(invoice.Metadata.BuyerEmail.AsString());
//
textSearch.Add(invoice.RefundMail);
// TODO: Are there more things to cache? PaymentData?

View File

@ -84,8 +84,8 @@ namespace BTCPayServer.Payments.Lightning
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
.Replace("{ItemDescription}", invoice.Metadata.ItemDesc.AsString() ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.Metadata.OrderId.AsString() ?? "", StringComparison.OrdinalIgnoreCase);
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
try

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.Plugins
public static bool IsExceptionByPlugin(Exception exception)
{
return _pluginAssemblies.Any(assembly => assembly.FullName.Contains(exception.Source));
return _pluginAssemblies.Any(assembly => assembly.FullName.Contains(exception.Source, StringComparison.OrdinalIgnoreCase));
}
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
IConfiguration config, ILoggerFactory loggerFactory)

View File

@ -98,8 +98,8 @@ namespace BTCPayServer.Services.Apps
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
var perkCount = paidInvoices
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode))
.GroupBy(entity => entity.Metadata.ItemCode)
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode.AsString()))
.GroupBy(entity => entity.Metadata.ItemCode.AsString())
.ToDictionary(entities => entities.Key, entities => entities.Count());
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);

View File

@ -87,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.Metadata.OrderId ?? string.Empty,
OrderId = invoice.Metadata.OrderId.AsString() ?? string.Empty,
StoreId = invoice.StoreId,
InvoiceId = invoice.Id,
InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime,
@ -98,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.Metadata.ItemCode,
InvoiceItemDesc = invoice.Metadata.ItemDesc,
InvoiceItemCode = invoice.Metadata.ItemCode.AsString(),
InvoiceItemDesc = invoice.Metadata.ItemDesc.AsString(),
InvoicePrice = invoice.Price,
InvoiceCurrency = invoice.Currency,
BuyerEmail = invoice.Metadata.BuyerEmail
BuyerEmail = invoice.Metadata.BuyerEmail.AsString()
};
exportList.Add(target);

View File

@ -30,67 +30,39 @@ namespace BTCPayServer.Services.Invoices
seria.ContractResolver = new CamelCasePropertyNamesContractResolver();
MetadataSerializer = seria;
}
public string OrderId { get; set; }
public JToken OrderId { get; set; }
[JsonProperty(PropertyName = "buyerName")]
public string BuyerName { get; set; }
public JToken BuyerName { get; set; }
[JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail { get; set; }
public JToken BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry { get; set; }
public JToken BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip { get; set; }
public JToken BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState")]
public string BuyerState { get; set; }
public JToken BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity { get; set; }
public JToken BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2 { get; set; }
public JToken BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1 { get; set; }
public JToken BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone { get; set; }
public JToken BuyerPhone { get; set; }
[JsonProperty(PropertyName = "itemDesc")]
public string ItemDesc { get; set; }
public JToken ItemDesc { get; set; }
[JsonProperty(PropertyName = "itemCode")]
public string ItemCode { get; set; }
public JToken ItemCode { get; set; }
[JsonProperty(PropertyName = "physical")]
public bool? Physical { get; set; }
public JToken Physical { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal? TaxIncluded { get; set; }
[JsonIgnore]
public string PosData
{
get
{
return PosRawData?.ToString();
}
set
{
if (value is null)
{
PosRawData = JValue.CreateNull();
}
else
{
try
{
PosRawData = JToken.Parse(value);
}
catch (Exception )
{
PosRawData = JToken.FromObject(value);
}
}
}
}
public JToken TaxIncluded { get; set; }
[JsonProperty(PropertyName = "posData")]
public JToken PosRawData { get; set; }
public JToken PosData { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
@ -283,9 +255,11 @@ namespace BTCPayServer.Services.Invoices
private Uri FillPlaceholdersUri(string v)
{
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"))
v ??= string.Empty;
string orderId = Metadata.OrderId.AsString();
v = v.Replace("{OrderId}", orderId is null ? string.Empty : System.Web.HttpUtility.UrlEncode(orderId), StringComparison.OrdinalIgnoreCase);
v = v.Replace("{InvoiceId}", System.Web.HttpUtility.UrlEncode(Id), StringComparison.OrdinalIgnoreCase);
if (Uri.TryCreate(v, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https"))
return uri;
return null;
}
@ -330,8 +304,8 @@ namespace BTCPayServer.Services.Invoices
{
Id = Id,
StoreId = StoreId,
OrderId = Metadata.OrderId,
PosData = Metadata.PosData,
OrderId = Metadata.OrderId.AsString(),
PosData = Metadata.PosData.AsString(),
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime,
@ -449,9 +423,9 @@ namespace BTCPayServer.Services.Invoices
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
dto.ItemCode = Metadata.ItemCode;
dto.ItemDesc = Metadata.ItemDesc;
dto.TaxIncluded = Metadata.TaxIncluded ?? 0m;
dto.ItemCode = Metadata.ItemCode.AsString();
dto.ItemDesc = Metadata.ItemDesc.AsString();
dto.TaxIncluded = Metadata.TaxIncluded.AsDecimal() ?? 0m;
dto.Price = Price;
dto.Currency = Currency;
dto.Buyer = new JObject();
@ -463,7 +437,7 @@ namespace BTCPayServer.Services.Invoices
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.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(Metadata.BuyerEmail.AsString()) ? RefundMail : Metadata.BuyerEmail));
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();

View File

@ -166,11 +166,11 @@ namespace BTCPayServer.Services.Invoices
Id = invoice.Id,
Created = invoice.InvoiceTime,
Blob = ToBytes(invoice, null),
OrderId = invoice.Metadata.OrderId,
OrderId = invoice.Metadata.OrderId.AsString(),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
ItemCode = invoice.Metadata.ItemCode,
ItemCode = invoice.Metadata.ItemCode.AsString(),
CustomerEmail = invoice.RefundMail,
Archived = false
};
@ -207,9 +207,9 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(invoice.Id);
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.Metadata.OrderId);
textSearch.Add(invoice.Metadata.OrderId.AsString());
textSearch.Add(invoice.StoreId);
textSearch.Add(invoice.Metadata.BuyerEmail);
textSearch.Add(invoice.Metadata.BuyerEmail.AsString());
AddToTextSearch(context, invoiceData, textSearch.ToArray());
await context.SaveChangesAsync().ConfigureAwait(false);
@ -597,7 +597,7 @@ namespace BTCPayServer.Services.Invoices
{
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
}
if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.Metadata.BuyerEmail))
if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.Metadata.BuyerEmail.AsString()))
{
entity.Metadata.BuyerEmail = entity.RefundMail;
}

View File

@ -150,14 +150,14 @@
{
<h3 class="mb-3">Product Information</h3>
<table class="table table-sm table-responsive-md removetopborder">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode.AsString()))
{
<tr>
<th>Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc.AsString()))
{
<tr>
<th>Item Description</th>
@ -183,14 +183,14 @@
<div class="col-md-6 mb-4">
<h3 class="mb-3">Product information</h3>
<table class="table table-sm table-responsive-md removetopborder">
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode.AsString()))
{
<tr>
<th>Item code</th>
<td>@Model.TypedMetadata.ItemCode</td>
</tr>
}
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc.AsString()))
{
<tr>
<th>Item Description</th>