mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Switch Apps to json not YML (#4792)
This commit is contained in:
parent
97e7e60cea
commit
8860eec254
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
@ -41,4 +43,7 @@ public class LightningAddressDataBlob
|
||||
public decimal? Max { get; set; }
|
||||
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -651,6 +652,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
@ -662,7 +664,6 @@ donation:
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
@ -723,6 +724,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
publicApps = user.GetController<UIPointOfSaleController>();
|
||||
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
@ -750,6 +752,8 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
@ -780,15 +784,13 @@ noninventoryitem:
|
||||
|
||||
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
|
||||
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
|
||||
//check that item is back in stock
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal(1,
|
||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
@ -803,6 +805,8 @@ btconly:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
@ -847,18 +851,19 @@ g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
var items = appService.Parse(vmpos.Template, vmpos.Currency);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
|
||||
var items = AppService.Parse(vmpos.Template);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -19,6 +21,74 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseOldYmlCorrectly()
|
||||
{
|
||||
var testOriginalDefaultYmlTemplate = @"
|
||||
green tea:
|
||||
price: 1
|
||||
title: Green Tea
|
||||
description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.
|
||||
image: ~/img/pos-sample/green-tea.jpg
|
||||
|
||||
black tea:
|
||||
price: 1
|
||||
title: Black Tea
|
||||
description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.
|
||||
image: ~/img/pos-sample/black-tea.jpg
|
||||
|
||||
rooibos:
|
||||
price: 1.2
|
||||
title: Rooibos
|
||||
description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.
|
||||
image: ~/img/pos-sample/rooibos.jpg
|
||||
|
||||
pu erh:
|
||||
price: 2
|
||||
title: Pu Erh
|
||||
description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.
|
||||
image: ~/img/pos-sample/pu-erh.jpg
|
||||
|
||||
herbal tea:
|
||||
price: 1.8
|
||||
title: Herbal Tea
|
||||
description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!
|
||||
image: ~/img/pos-sample/herbal-tea.jpg
|
||||
custom: true
|
||||
|
||||
fruit tea:
|
||||
price: 1.5
|
||||
title: Fruit Tea
|
||||
description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!
|
||||
image: ~/img/pos-sample/fruit-tea.jpg
|
||||
inventory: 5
|
||||
custom: true
|
||||
";
|
||||
var parsedDefault = MigrationStartupTask.ParsePOSYML(testOriginalDefaultYmlTemplate);
|
||||
Assert.Equal(6, parsedDefault.Length);
|
||||
Assert.Equal( "Green Tea" ,parsedDefault[0].Title);
|
||||
Assert.Equal( "green tea" ,parsedDefault[0].Id);
|
||||
Assert.Equal( "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years." ,parsedDefault[0].Description);
|
||||
Assert.Null( parsedDefault[0].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
|
||||
Assert.Equal( 1 ,parsedDefault[0].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Null( parsedDefault[0].AdditionalData);
|
||||
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||
|
||||
|
||||
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||
Assert.Equal( "herbal tea" ,parsedDefault[4].Id);
|
||||
Assert.Equal( "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!" ,parsedDefault[4].Description);
|
||||
Assert.Null( parsedDefault[4].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
|
||||
Assert.Equal( 1.8m ,parsedDefault[4].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePoSApp1()
|
||||
@ -53,6 +123,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
|
@ -964,7 +964,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
|
||||
|
||||
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
|
||||
Assert.Contains("buyButtonText: Take my money", template);
|
||||
Assert.Contains("\"buyButtonText\":\"Take my money\"", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
@ -45,6 +45,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.24" />
|
||||
@ -75,7 +76,6 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||
</ItemGroup>
|
||||
|
@ -245,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
|
||||
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
|
||||
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||
@ -272,7 +272,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
Currency = request.Currency,
|
||||
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : null,
|
||||
Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null,
|
||||
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
@ -331,7 +331,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Currency = settings.Currency,
|
||||
Items = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
_appService.Parse(settings.Template, settings.Currency),
|
||||
AppService.Parse(settings.Template),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
@ -363,8 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -406,7 +406,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Tagline = settings.Tagline,
|
||||
Perks = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
_appService.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
||||
AppService.Parse(settings.PerksTemplate),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
@ -453,8 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -257,12 +257,12 @@ namespace BTCPayServer
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||
currencyCode = cfS.TargetCurrency;
|
||||
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
||||
items = AppService.Parse(cfS.PerksTemplate);
|
||||
break;
|
||||
case PointOfSaleAppType.AppType:
|
||||
posS = app.GetSettings<PointOfSaleSettings>();
|
||||
currencyCode = posS.Currency;
|
||||
items = _appService.Parse(posS.Template, posS.Currency);
|
||||
items = AppService.Parse(posS.Template);
|
||||
break;
|
||||
default:
|
||||
//TODO: Allow other apps to define lnurl support
|
||||
|
@ -40,11 +40,11 @@ namespace BTCPayServer.HostedServices
|
||||
case PointOfSaleAppType.AppType:
|
||||
var possettings = data.GetSettings<PointOfSaleSettings>();
|
||||
return (Data: data, Settings: (object)possettings,
|
||||
Items: _appService.Parse(possettings.Template, possettings.Currency));
|
||||
Items: AppService.Parse(possettings.Template));
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
||||
return (Data: data, Settings: (object)cfsettings,
|
||||
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
|
||||
Items: AppService.Parse(cfsettings.PerksTemplate));
|
||||
default:
|
||||
return (null, null, null);
|
||||
}
|
||||
@ -70,11 +70,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
case PointOfSaleAppType.AppType:
|
||||
((PointOfSaleSettings)valueTuple.Settings).Template =
|
||||
_appService.SerializeTemplate(valueTuple.Items);
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
break;
|
||||
case CrowdfundAppType.AppType:
|
||||
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
||||
_appService.SerializeTemplate(valueTuple.Items);
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -26,11 +27,14 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBXplorer;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PeterO.Cbor;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using YamlDotNet.Serialization;
|
||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||
using Serializer = NBXplorer.Serializer;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -246,6 +250,11 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
await FixMappedDomainAppType();
|
||||
settings.FixMappedDomainAppType = true;
|
||||
}
|
||||
if (!settings.MigrateAppYmlToJson)
|
||||
{
|
||||
await MigrateAppYmlToJson();
|
||||
settings.MigrateAppYmlToJson = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
@ -304,6 +313,134 @@ namespace BTCPayServer.Hosting
|
||||
return;
|
||||
await ToPostgresMigrationStartupTask.UpdateSequenceInvoiceSearch(ctx);
|
||||
}
|
||||
private async Task MigrateAppYmlToJson()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
var apps = await ctx.Apps.Where(data => CrowdfundAppType.AppType == data.AppType || PointOfSaleAppType.AppType == data.AppType)
|
||||
.ToListAsync();
|
||||
foreach (var app in apps)
|
||||
{
|
||||
switch (app.AppType)
|
||||
{
|
||||
case CrowdfundAppType.AppType :
|
||||
var cfSettings = app.GetSettings<CrowdfundSettings>();
|
||||
if (!string.IsNullOrEmpty(cfSettings?.PerksTemplate))
|
||||
{
|
||||
cfSettings.PerksTemplate = AppService.SerializeTemplate(ParsePOSYML(cfSettings?.PerksTemplate));
|
||||
app.SetSettings(cfSettings);
|
||||
}
|
||||
break;
|
||||
case PointOfSaleAppType.AppType:
|
||||
var pSettings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (!string.IsNullOrEmpty(pSettings?.Template))
|
||||
{
|
||||
pSettings.Template = AppService.SerializeTemplate(ParsePOSYML(pSettings?.Template));
|
||||
app.SetSettings(pSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] ParsePOSYML(string yaml)
|
||||
{
|
||||
var items = new List<ViewPointOfSaleViewModel.Item>();
|
||||
var stream = new YamlStream();
|
||||
stream.Load(new StringReader(yaml));
|
||||
|
||||
var root = stream.Documents.First().RootNode as YamlMappingNode;
|
||||
foreach (var posItem in root.Children)
|
||||
{
|
||||
var trimmedKey = ((YamlScalarNode)posItem.Key).Value?.Trim();
|
||||
if (string.IsNullOrEmpty(trimmedKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentItem = new ViewPointOfSaleViewModel.Item
|
||||
{
|
||||
Id = trimmedKey, Title = trimmedKey, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed
|
||||
};
|
||||
var itemSpecs = (YamlMappingNode)posItem.Value;
|
||||
foreach (var spec in itemSpecs)
|
||||
{
|
||||
if (spec.Key is not YamlScalarNode {Value: string keyString} || string.IsNullOrEmpty(keyString))
|
||||
continue;
|
||||
var scalarValue = spec.Value as YamlScalarNode;
|
||||
switch (keyString)
|
||||
{
|
||||
case "title":
|
||||
currentItem.Title = scalarValue?.Value ?? trimmedKey;
|
||||
break;
|
||||
case "inventory":
|
||||
if (int.TryParse(scalarValue?.Value, out var inv))
|
||||
{
|
||||
currentItem.Inventory = inv;
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
currentItem.Description = scalarValue?.Value;
|
||||
break;
|
||||
case "image":
|
||||
currentItem.Image = scalarValue?.Value;
|
||||
break;
|
||||
case "payment_methods" when spec.Value is YamlSequenceNode pmSequenceNode:
|
||||
|
||||
currentItem.PaymentMethods = pmSequenceNode.Children
|
||||
.Select(node => (node as YamlScalarNode)?.Value?.Trim())
|
||||
.Where(node => !string.IsNullOrEmpty(node)).ToArray();
|
||||
break;
|
||||
case "price_type":
|
||||
case "custom":
|
||||
if (bool.TryParse(scalarValue?.Value, out var customBoolValue))
|
||||
{
|
||||
if (customBoolValue)
|
||||
{
|
||||
currentItem.PriceType = currentItem.Price is null or 0
|
||||
? ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
: ViewPointOfSaleViewModel.ItemPriceType.Minimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentItem.PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
}
|
||||
}
|
||||
else if (Enum.TryParse<ViewPointOfSaleViewModel.ItemPriceType>(scalarValue?.Value, true,
|
||||
out var customPriceType))
|
||||
{
|
||||
currentItem.PriceType = customPriceType;
|
||||
}
|
||||
|
||||
break;
|
||||
case "price":
|
||||
if (decimal.TryParse(scalarValue?.Value, out var price))
|
||||
{
|
||||
currentItem.Price = price;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "buybuttontext":
|
||||
currentItem.BuyButtonText = scalarValue?.Value;
|
||||
break;
|
||||
|
||||
case "disabled":
|
||||
if (bool.TryParse(scalarValue?.Value, out var disabled))
|
||||
{
|
||||
currentItem.Disabled = disabled;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
items.Add(currentItem);
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|
||||
@ -521,8 +658,8 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings1.TargetCurrency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings1);
|
||||
}
|
||||
items = _appService.Parse(settings1.PerksTemplate, settings1.TargetCurrency);
|
||||
newTemplate = _appService.SerializeTemplate(items);
|
||||
items = AppService.Parse(settings1.PerksTemplate);
|
||||
newTemplate = AppService.SerializeTemplate(items);
|
||||
if (settings1.PerksTemplate != newTemplate)
|
||||
{
|
||||
settings1.PerksTemplate = newTemplate;
|
||||
@ -538,8 +675,8 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings2);
|
||||
}
|
||||
items = _appService.Parse(settings2.Template, settings2.Currency);
|
||||
newTemplate = _appService.SerializeTemplate(items);
|
||||
items = AppService.Parse(settings2.Template);
|
||||
newTemplate = AppService.SerializeTemplate(items);
|
||||
if (settings2.Template != newTemplate)
|
||||
{
|
||||
settings2.Template = newTemplate;
|
||||
|
@ -127,13 +127,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _appService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
|
||||
var choices = AppService.Parse(settings.PerksTemplate, false);
|
||||
choice = choices?.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
|
||||
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
@ -273,7 +273,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
vm.PerksTemplate = _appService.SerializeTemplate(_appService.Parse(vm.PerksTemplate, vm.TargetCurrency));
|
||||
vm.PerksTemplate = AppService.SerializeTemplate(AppService.Parse(vm.PerksTemplate));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -74,14 +74,14 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||
{
|
||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, cfS.PerksTemplate, cfS.TargetCurrency);
|
||||
var items = AppService.Parse( cfS.PerksTemplate);
|
||||
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
var perks = AppService.Parse(_htmlSanitizer, _displayFormatter, settings.PerksTemplate, settings.TargetCurrency);
|
||||
var perks = AppService.Parse( settings.PerksTemplate);
|
||||
var perkCount = paidInvoices
|
||||
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
||||
// we need the item code to know which perk it is and group by that
|
||||
@ -176,7 +176,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
})));
|
||||
}
|
||||
|
||||
var perks = AppService.GetPOSItems(_htmlSanitizer, _displayFormatter, settings.PerksTemplate, settings.TargetCurrency);
|
||||
var perks = AppService.Parse( settings.PerksTemplate, false);
|
||||
if (settings.SortPerksByPopularity)
|
||||
{
|
||||
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
||||
|
@ -106,7 +106,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
|
||||
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
|
||||
},
|
||||
Items = _appService.GetPOSItems(settings.Template, settings.Currency),
|
||||
Items = AppService.Parse(settings.Template, false),
|
||||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
@ -165,12 +165,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
ViewPointOfSaleViewModel.Item[] choices = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
choices = _appService.GetPOSItems(settings.Template, settings.Currency);
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
@ -204,7 +204,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (currentView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
{
|
||||
choices = _appService.GetPOSItems(settings.Template, settings.Currency);
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
var expectedMinimumAmount = 0m;
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
@ -224,9 +224,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
|
||||
decimal expectedCartItemPrice = 0;
|
||||
if (itemChoice.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
if (itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
expectedCartItemPrice = itemChoice.Price.Value ?? 0;
|
||||
expectedCartItemPrice = itemChoice.Price ?? 0;
|
||||
}
|
||||
|
||||
expectedMinimumAmount += expectedCartItemPrice * cartItem.Value;
|
||||
@ -327,7 +327,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (selectedChoices.TryGetValue(cartItem.Key, out var selectedChoice))
|
||||
{
|
||||
cartData.Add(selectedChoice.Title ?? selectedChoice.Id,
|
||||
$"{(selectedChoice.Price.Value is null ? "Any price" : $"{_displayFormatter.Currency((decimal)selectedChoice.Price.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")} x {cartItem.Value} = {(selectedChoice.Price.Value is null ? "Any price" : $"{_displayFormatter.Currency(((decimal)selectedChoice.Price.Value) * cartItem.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")}");
|
||||
$"{(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency((decimal)selectedChoice.Price.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")} x {cartItem.Value} = {(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency(((decimal)selectedChoice.Price.Value) * cartItem.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")}");
|
||||
|
||||
}
|
||||
}
|
||||
@ -533,7 +533,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = _appService.Parse(settings.Template, settings.Currency);
|
||||
var items = AppService.Parse(settings.Template);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
@ -568,7 +568,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
vm.Template = _appService.SerializeTemplate(_appService.Parse(vm.Template, vm.Currency));
|
||||
vm.Template = AppService.SerializeTemplate(AppService.Parse(vm.Template));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -1,34 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
{
|
||||
public class ViewPointOfSaleViewModel
|
||||
{
|
||||
public enum ItemPriceType
|
||||
{
|
||||
Topup,
|
||||
Minimum,
|
||||
Fixed
|
||||
}
|
||||
|
||||
public class Item
|
||||
{
|
||||
public class ItemPrice
|
||||
{
|
||||
public enum ItemPriceType
|
||||
{
|
||||
Topup,
|
||||
Minimum,
|
||||
Fixed
|
||||
}
|
||||
|
||||
public ItemPriceType Type { get; set; }
|
||||
public string Formatted { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Description { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Image { get; set; }
|
||||
public ItemPrice Price { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public ItemPriceType PriceType { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Price { get; set; }
|
||||
public string Title { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string BuyButtonText { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int? Inventory { get; set; } = null;
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public bool Disabled { get; set; } = false;
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyInfoData
|
||||
|
@ -82,14 +82,14 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||
public Task<SalesStats> GetSalesStats(AppData app, InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||
{
|
||||
var posS = app.GetSettings<PointOfSaleSettings>();
|
||||
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, posS.Template, posS.Currency);
|
||||
var items = AppService.Parse(posS.Template);
|
||||
return AppService.GetSalesStatswithPOSItems(items, paidInvoices, numberOfDays);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<ItemStats>> GetItemStats(AppData appData, InvoiceEntity[] paidInvoices)
|
||||
{
|
||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||
var items = AppService.Parse(_htmlSanitizer, _displayFormatter, settings.Template, settings.Currency);
|
||||
var items = AppService.Parse( settings.Template);
|
||||
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
|
||||
|
@ -13,18 +13,12 @@ using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using ExchangeSharp;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using YamlDotNet.Serialization;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
@ -32,6 +26,17 @@ namespace BTCPayServer.Services.Apps
|
||||
public class AppService
|
||||
{
|
||||
private readonly Dictionary<string, AppBaseType> _appTypes;
|
||||
static AppService()
|
||||
{
|
||||
_defaultSerializer = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings _defaultSerializer;
|
||||
|
||||
readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
readonly CurrencyNameTable _Currencies;
|
||||
@ -342,169 +347,20 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
return _storeRepository.FindStore(app.StoreDataId);
|
||||
}
|
||||
|
||||
|
||||
public string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items)
|
||||
public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items)
|
||||
{
|
||||
var mappingNode = new YamlMappingNode();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemNode = new YamlMappingNode();
|
||||
itemNode.Add("title", new YamlScalarNode(item.Title));
|
||||
if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup && item.Price.Value is not null)
|
||||
itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
|
||||
if (!string.IsNullOrEmpty(item.Description))
|
||||
{
|
||||
itemNode.Add("description", new YamlScalarNode(item.Description)
|
||||
{
|
||||
Style = ScalarStyle.DoubleQuoted
|
||||
});
|
||||
}
|
||||
if (!string.IsNullOrEmpty(item.Image))
|
||||
{
|
||||
itemNode.Add("image", new YamlScalarNode(item.Image));
|
||||
}
|
||||
itemNode.Add("price_type", new YamlScalarNode(item.Price.Type.ToStringLowerInvariant()));
|
||||
itemNode.Add("disabled", new YamlScalarNode(item.Disabled.ToStringLowerInvariant()));
|
||||
if (item.Inventory.HasValue)
|
||||
{
|
||||
itemNode.Add("inventory", new YamlScalarNode(item.Inventory.ToString()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.BuyButtonText))
|
||||
{
|
||||
itemNode.Add("buyButtonText", new YamlScalarNode(item.BuyButtonText));
|
||||
}
|
||||
|
||||
if (item.PaymentMethods?.Any() is true)
|
||||
{
|
||||
itemNode.Add("payment_methods", new YamlSequenceNode(item.PaymentMethods.Select(s => new YamlScalarNode(s))));
|
||||
}
|
||||
mappingNode.Add(item.Id, itemNode);
|
||||
}
|
||||
|
||||
var serializer = new SerializerBuilder().Build();
|
||||
return serializer.Serialize(mappingNode);
|
||||
return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer);
|
||||
}
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
return Parse(_HtmlSanitizer, _displayFormatter, template, currency);
|
||||
}
|
||||
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] GetPOSItems(string template, string currency)
|
||||
{
|
||||
return GetPOSItems(_HtmlSanitizer, _displayFormatter, template, currency);
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] Parse(HtmlSanitizer htmlSanitizer, DisplayFormatter displayFormatter, string template, string currency)
|
||||
public static ViewPointOfSaleViewModel.Item[] Parse(string template, bool includeDisabled = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return Array.Empty<ViewPointOfSaleViewModel.Item>();
|
||||
using var input = new StringReader(template);
|
||||
YamlStream stream = new();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new PosHolder(htmlSanitizer) { Key = htmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c =>
|
||||
{
|
||||
ViewPointOfSaleViewModel.Item.ItemPrice price = new();
|
||||
var pValue = c.GetDetail("price")?.FirstOrDefault();
|
||||
|
||||
switch (c.GetDetailString("custom") ?? c.GetDetailString("price_type")?.ToLowerInvariant())
|
||||
{
|
||||
case "topup":
|
||||
case null when pValue is null:
|
||||
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup;
|
||||
break;
|
||||
case "true":
|
||||
case "minimum":
|
||||
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum;
|
||||
if (pValue != null && !string.IsNullOrEmpty(pValue.Value?.Value))
|
||||
{
|
||||
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
||||
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
}
|
||||
break;
|
||||
case "fixed":
|
||||
case "false":
|
||||
case null:
|
||||
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
|
||||
if (pValue?.Value.Value is not null)
|
||||
{
|
||||
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
|
||||
price.Formatted = displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return new ViewPointOfSaleViewModel.Item
|
||||
{
|
||||
Description = c.GetDetailString("description"),
|
||||
Id = c.Key,
|
||||
Image = c.GetDetailString("image"),
|
||||
Title = c.GetDetailString("title") ?? c.Key,
|
||||
Price = price,
|
||||
BuyButtonText = c.GetDetailString("buyButtonText"),
|
||||
Inventory =
|
||||
string.IsNullOrEmpty(c.GetDetailString("inventory"))
|
||||
? null
|
||||
: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
|
||||
PaymentMethods = c.GetDetailStringList("payment_methods"),
|
||||
Disabled = c.GetDetailString("disabled") == "true"
|
||||
};
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static ViewPointOfSaleViewModel.Item[] GetPOSItems(HtmlSanitizer htmlSanitizer, DisplayFormatter displayFormatter, string template, string currency)
|
||||
{
|
||||
return Parse(htmlSanitizer, displayFormatter, template, currency).Where(c => !c.Disabled).ToArray();
|
||||
return JsonConvert.DeserializeObject<ViewPointOfSaleViewModel.Item[]>(template, _defaultSerializer)!.Where(item => includeDisabled || !item.Disabled).ToArray();
|
||||
}
|
||||
#nullable restore
|
||||
private class PosHolder
|
||||
{
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
|
||||
public PosHolder(
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
}
|
||||
|
||||
public string Key { get; set; }
|
||||
public YamlMappingNode Value { get; set; }
|
||||
|
||||
public IEnumerable<PosScalar> GetDetail(string field)
|
||||
{
|
||||
var res = Value.Children
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(cc => cc.Key == field);
|
||||
return res;
|
||||
}
|
||||
|
||||
public string GetDetailString(string field)
|
||||
{
|
||||
var raw = GetDetail(field).FirstOrDefault()?.Value?.Value;
|
||||
return raw is null ? null : _htmlSanitizer.Sanitize(raw);
|
||||
}
|
||||
public string[] GetDetailStringList(string field)
|
||||
{
|
||||
if (!Value.Children.ContainsKey(field) || !(Value.Children[field] is YamlSequenceNode sequenceNode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return sequenceNode.Children.Select(node => (node as YamlScalarNode)?.Value).Where(s => s != null).Select(s => _htmlSanitizer.Sanitize(s)).ToArray();
|
||||
}
|
||||
}
|
||||
private class PosScalar
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public YamlScalarNode Value { get; set; }
|
||||
}
|
||||
#nullable enable
|
||||
public async Task<AppData?> GetAppDataIfOwner(string userId, string appId, string? type = null)
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
@ -9,40 +9,71 @@ namespace BTCPayServer.Services.Apps
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "Tea shop";
|
||||
Template =
|
||||
"green tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Green Tea\n" +
|
||||
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
|
||||
" image: ~/img/pos-sample/green-tea.jpg\n\n" +
|
||||
"black tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Black Tea\n" +
|
||||
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
|
||||
" image: ~/img/pos-sample/black-tea.jpg\n\n" +
|
||||
"rooibos:\n" +
|
||||
" price: 1.2\n" +
|
||||
" title: Rooibos\n" +
|
||||
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
|
||||
" image: ~/img/pos-sample/rooibos.jpg\n\n" +
|
||||
"pu erh:\n" +
|
||||
" price: 2\n" +
|
||||
" title: Pu Erh\n" +
|
||||
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
|
||||
" image: ~/img/pos-sample/pu-erh.jpg\n\n" +
|
||||
"herbal tea:\n" +
|
||||
" price: 1.8\n" +
|
||||
" title: Herbal Tea\n" +
|
||||
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
|
||||
" image: ~/img/pos-sample/herbal-tea.jpg\n" +
|
||||
" custom: true\n\n" +
|
||||
"fruit tea:\n" +
|
||||
" price: 1.5\n" +
|
||||
" title: Fruit Tea\n" +
|
||||
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
|
||||
" image: ~/img/pos-sample/fruit-tea.jpg\n" +
|
||||
" inventory: 5\n" +
|
||||
" custom: true";
|
||||
Template = AppService.SerializeTemplate(new ViewPointOfSaleViewModel.Item[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "green-tea",
|
||||
Title = "Green Tea",
|
||||
Description =
|
||||
"Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.",
|
||||
Image = "~/img/pos-sample/green-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 1
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "black-tea",
|
||||
Title = "Black Tea",
|
||||
Description =
|
||||
"Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.",
|
||||
Image = "~/img/pos-sample/black-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 1
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "rooibos",
|
||||
Title = "Rooibos",
|
||||
Description =
|
||||
"Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.",
|
||||
Image = "~/img/pos-sample/rooibos.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 1.2m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "pu-erh",
|
||||
Title = "Pu Erh",
|
||||
Description =
|
||||
"This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.",
|
||||
Image = "~/img/pos-sample/pu-erh.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 2
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "herbal-tea",
|
||||
Title = "Herbal Tea",
|
||||
Description =
|
||||
"Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!",
|
||||
Image = "~/img/pos-sample/herbal-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Minimum,
|
||||
Price = 1.8m,
|
||||
Disabled = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "fruit-tea",
|
||||
Title = "Fruit Tea",
|
||||
Description =
|
||||
"The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!",
|
||||
Image = "~/img/pos-sample/fruit-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup,
|
||||
Inventory = 5,
|
||||
Disabled = true
|
||||
}
|
||||
});
|
||||
DefaultView = PosViewType.Static;
|
||||
ShowCustomAmount = false;
|
||||
ShowDiscount = true;
|
||||
|
@ -58,5 +58,5 @@ public class PosAppCartItemPrice
|
||||
public decimal Value { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType Type { get; set; }
|
||||
public ViewPointOfSaleViewModel.ItemPriceType Type { get; set; }
|
||||
}
|
||||
|
@ -37,5 +37,6 @@ namespace BTCPayServer.Services
|
||||
public bool FileSystemStorageAsDefault { get; set; }
|
||||
public bool FixSeqAfterSqliteMigration { get; set; }
|
||||
public bool FixMappedDomainAppType { get; set; }
|
||||
public bool MigrateAppYmlToJson { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -33,15 +33,15 @@
|
||||
<span>@item.Price.Value</span>
|
||||
<span>@vm.TargetCurrency</span>
|
||||
|
||||
if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum)
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum)
|
||||
{
|
||||
@Safe.Raw("or more")
|
||||
}
|
||||
}
|
||||
else if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed )
|
||||
else if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed )
|
||||
{
|
||||
@Safe.Raw("Any amount")
|
||||
}else if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed)
|
||||
}else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed)
|
||||
{
|
||||
@Safe.Raw("Free")
|
||||
}
|
||||
@ -56,7 +56,7 @@
|
||||
{
|
||||
case null:
|
||||
break;
|
||||
case var i when i <= 0:
|
||||
case <= 0:
|
||||
<span>Sold out</span>
|
||||
break;
|
||||
default:
|
||||
|
@ -1,5 +1,7 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
var customTipPercentages = Model.CustomTipPercentages;
|
||||
@ -253,11 +255,12 @@
|
||||
|
||||
<span class="text-muted small">
|
||||
@{
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
buttonText = buttonText.Replace("{0}",item.Price.Formatted)
|
||||
?.Replace("{Price}",item.Price.Formatted);
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
buttonText = buttonText.Replace("{0}",formatted)
|
||||
?.Replace("{Price}",formatted);
|
||||
}
|
||||
}
|
||||
@Safe.Raw(buttonText)
|
||||
|
@ -1,11 +1,12 @@
|
||||
@using BTCPayServer.Payments.Lightning
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Stores
|
||||
@using LNURL
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@inject StoreRepository StoreRepository
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
var store = await StoreRepository.FindStore(Model.StoreId);
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
@ -68,11 +69,7 @@ else
|
||||
Description = "Create invoice to pay custom amount",
|
||||
Title = "Custom amount",
|
||||
BuyButtonText = Model.CustomButtonText,
|
||||
Price = new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup,
|
||||
|
||||
}
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
}
|
||||
}).ToArray();
|
||||
}
|
||||
@ -90,16 +87,19 @@ else
|
||||
<p class="card-title text-center">@Safe.Raw(item.Description)</p>
|
||||
}
|
||||
<div class="w-100 mb-3 fs-5 text-center">
|
||||
@switch (item.Price.Type)
|
||||
@{
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
}
|
||||
@switch (item.PriceType)
|
||||
{
|
||||
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup:
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Topup:
|
||||
<span>Any amount</span>
|
||||
break;
|
||||
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum:
|
||||
<span>@item.Price.Formatted minimum</span>
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Minimum:
|
||||
<span>@formatted minimum</span>
|
||||
break;
|
||||
case ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed:
|
||||
@item.Price.Formatted
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Fixed:
|
||||
@formatted
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
@ -1,5 +1,7 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||
@ -17,8 +19,9 @@
|
||||
@for (var x = 0; x < Model.Items.Length; x++)
|
||||
{
|
||||
var item = Model.Items[x];
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", item.Price.Formatted).Replace("{Price}", item.Price.Formatted);
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
|
||||
|
||||
<div class="card px-0" data-id="@x">
|
||||
@if (!string.IsNullOrWhiteSpace(item.Image))
|
||||
@ -29,11 +32,11 @@
|
||||
<div class="card-footer bg-transparent border-0 pb-3">
|
||||
@if (!item.Inventory.HasValue || item.Inventory.Value > 0)
|
||||
{
|
||||
@if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
@if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy autocomplete="off">
|
||||
<input type="hidden" name="choiceKey" value="@item.Id" />
|
||||
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
|
||||
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.PriceType, item.Price.Value, item.Price.Value);}
|
||||
</form>
|
||||
}
|
||||
else
|
||||
@ -72,7 +75,7 @@
|
||||
@{CardBody("Custom Amount", "Create invoice to pay custom amount");}
|
||||
<div class="card-footer bg-transparent border-0 pb-3">
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
|
||||
@{PayFormInputContent(Model.CustomButtonText, ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);}
|
||||
@{PayFormInputContent(Model.CustomButtonText, ViewPointOfSaleViewModel.ItemPriceType.Minimum);}
|
||||
</form>
|
||||
@if (anyInventoryItems)
|
||||
{
|
||||
@ -91,9 +94,9 @@
|
||||
</div>
|
||||
|
||||
@functions {
|
||||
private void PayFormInputContent(string buttonText,ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType itemPriceType, decimal? minPriceValue = null, decimal? priceValue = null)
|
||||
private void PayFormInputContent(string buttonText,ViewPointOfSaleViewModel.ItemPriceType itemPriceType, decimal? minPriceValue = null, decimal? priceValue = null)
|
||||
{
|
||||
if (itemPriceType == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed && priceValue == 0)
|
||||
if (itemPriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && priceValue == 0)
|
||||
{
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" readonly value="Free"/>
|
||||
@ -105,7 +108,7 @@
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
|
||||
<input class="form-control" type="number" min="@(minPriceValue ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@priceValue" readonly="@(itemPriceType == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed)">
|
||||
<input class="form-control" type="number" min="@(minPriceValue ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@priceValue" readonly="@(itemPriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed)">
|
||||
<button class="btn btn-primary text-nowrap" type="submit">@buttonText</button>
|
||||
</div>
|
||||
}
|
||||
|
@ -61,11 +61,11 @@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Price</label>
|
||||
<select class="form-select" v-model="editingItem.custom">
|
||||
<select class="form-select" v-model="editingItem.priceType">
|
||||
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6" v-show="editingItem.custom !== 'topup'">
|
||||
<div class="col-sm-6" v-show="editingItem.priceType !== 'Topup'">
|
||||
<label class="form-label"> </label>
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control"
|
||||
@ -129,9 +129,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
items: [],
|
||||
editingItem: null,
|
||||
customPriceOptions: [
|
||||
{ text: 'Fixed', value: "fixed" },
|
||||
{ text: 'Minimum', value: "minimum" },
|
||||
{ text: 'Custom', value: 'topup' },
|
||||
{ text: 'Fixed', value: "Fixed" },
|
||||
{ text: 'Minimum', value: "Minimum" },
|
||||
{ text: 'Custom', value: 'Topup' },
|
||||
],
|
||||
elementId: "@Model.templateId"
|
||||
},
|
||||
@ -141,8 +141,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.loadYml();
|
||||
this.getInputElement().on("input change", this.loadYml.bind(this));
|
||||
this.load();
|
||||
this.getInputElement().on("input change", this.load.bind(this));
|
||||
this.getModalElement().on("hide.bs.modal", this.clearEditingItem.bind(this));
|
||||
},
|
||||
methods: {
|
||||
@ -156,148 +156,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
},
|
||||
getInputElement : function(){ return $("#" + this.elementId); },
|
||||
getModalElement : function(){ return $("#product-modal"); },
|
||||
loadYml: function(){
|
||||
var result = [];
|
||||
var template = this.getInputElement().val().trim();
|
||||
var lines = [];
|
||||
var items = template.split("\n");
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i] === ""){
|
||||
continue;
|
||||
}
|
||||
if (items[i].startsWith(" ")){
|
||||
lines[lines.length-1]+=items[i] + "\n";
|
||||
} else {
|
||||
lines.push(items[i] + "\n");
|
||||
}
|
||||
load: function(){
|
||||
const template = this.getInputElement().val().trim();
|
||||
if (!template){
|
||||
this.items = [];
|
||||
} else {
|
||||
this.items = JSON.parse(template);
|
||||
}
|
||||
|
||||
// Split products from the template
|
||||
for (var kl in lines) {
|
||||
var line = lines[kl], product = line.split("\n"), goingThroughMethods = false,
|
||||
id = null, price = null, title = null, description = null, image = null,
|
||||
custom = null, buyButtonText = null, inventory = null, paymentMethods = [],
|
||||
disabled = false;
|
||||
|
||||
for (var kp in product) {
|
||||
var productProperty = product[kp].trim();
|
||||
|
||||
if (kp == 0) {
|
||||
id = productProperty.replace(":", "");
|
||||
}
|
||||
if (productProperty.startsWith("-") && goingThroughMethods) {
|
||||
paymentMethods.push(productProperty.substr(1));
|
||||
} else {
|
||||
goingThroughMethods = false;
|
||||
}
|
||||
|
||||
if (productProperty.indexOf('price:') !== -1) {
|
||||
price = parseFloat(productProperty.replace('price:', '').trim()).noExponents();
|
||||
}
|
||||
if (productProperty.indexOf('title:') !== -1) {
|
||||
title = productProperty.replace('title:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('description:') !== -1) {
|
||||
description =productProperty.replace('description:', '').trim();
|
||||
if (description.startsWith('"') && description.endsWith('"')){
|
||||
description = description.substr(1, description.length-2);
|
||||
}
|
||||
description = description
|
||||
.replaceAll("<br>", "\n")
|
||||
.replaceAll("<br/>", "\n")
|
||||
.replaceAll('\\"', '"')
|
||||
}
|
||||
if (productProperty.indexOf('image:') !== -1) {
|
||||
image = productProperty.replace('image:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('price_type:') !== -1) {
|
||||
custom = productProperty.replace('price_type:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('buyButtonText:') !== -1) {
|
||||
buyButtonText = productProperty.replace('buyButtonText:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('inventory:') !== -1) {
|
||||
inventory = parseInt(productProperty.replace('inventory:', '').trim(),10);
|
||||
}
|
||||
if (productProperty.indexOf('payment_methods:') !== -1) {
|
||||
goingThroughMethods = true;
|
||||
}
|
||||
if (productProperty.indexOf('disabled:') !== -1) {
|
||||
disabled = productProperty.replace('disabled:', '').trim() === "true";
|
||||
}
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
// Add product to the list
|
||||
result.push({
|
||||
id: id,
|
||||
title: title,
|
||||
price: price,
|
||||
image: image || null,
|
||||
description: description || '',
|
||||
custom: custom || "fixed",
|
||||
buyButtonText: buyButtonText,
|
||||
inventory: isNaN(inventory)? null: inventory,
|
||||
paymentMethods: paymentMethods,
|
||||
disabled: disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
this.items = result;
|
||||
},
|
||||
toYml: function(){
|
||||
let template = '';
|
||||
// Construct template from the product list
|
||||
for (const key in this.items) {
|
||||
const product = this.items[key],
|
||||
id = product.id,
|
||||
title = product.title,
|
||||
price = product.custom === 'topup'? null : product.price??0,
|
||||
image = product.image,
|
||||
description = product.description,
|
||||
custom = product.custom,
|
||||
buyButtonText = product.buyButtonText,
|
||||
inventory = product.inventory,
|
||||
paymentMethods = product.paymentMethods,
|
||||
disabled = product.disabled;
|
||||
let itemTemplate = id+":\n";
|
||||
itemTemplate += ( product.custom === 'topup'? '' : (' price: ' + parseFloat(price).noExponents() + '\n'));
|
||||
itemTemplate+= ' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
itemTemplate += ' description: "' + description.replaceAll("\n", "<br/>").replaceAll('"', '\\"') + '"\n';
|
||||
}
|
||||
if (image) {
|
||||
itemTemplate += ' image: ' + image + '\n';
|
||||
}
|
||||
if (inventory) {
|
||||
itemTemplate += ' inventory: ' + inventory + '\n';
|
||||
}
|
||||
if (custom != null) {
|
||||
itemTemplate += ' price_type: "' + custom + '" \n';
|
||||
}
|
||||
if (buyButtonText != null && buyButtonText.length > 0) {
|
||||
itemTemplate += ' buyButtonText: ' + buyButtonText + '\n';
|
||||
}
|
||||
if (disabled != null) {
|
||||
itemTemplate += ' disabled: ' + disabled.toString() + '\n';
|
||||
}
|
||||
if(paymentMethods != null && paymentMethods.length > 0){
|
||||
itemTemplate+= ' payment_methods:\n';
|
||||
for (var method of paymentMethods){
|
||||
itemTemplate+= ' - '+method+'\n';
|
||||
}
|
||||
}
|
||||
itemTemplate += '\n';
|
||||
template+=itemTemplate;
|
||||
}
|
||||
this.getInputElement().val(template);
|
||||
save: function(){
|
||||
let template = JSON.stringify(this.items);
|
||||
this.getInputElement().val(template);
|
||||
},
|
||||
editItem: function(index){
|
||||
this.errors = [];
|
||||
if(index < 0){
|
||||
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: "fixed", inventory: null, paymentMethods: [], disabled: false};
|
||||
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", priceType: "Fixed", inventory: null, disabled: false};
|
||||
}else{
|
||||
this.editingItem = {...this.items[index], index};
|
||||
}
|
||||
@ -307,7 +181,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
},
|
||||
removeItem: function(index){
|
||||
this.items.splice(index,1);
|
||||
this.toYml();
|
||||
this.save();
|
||||
},
|
||||
clearEditingItem: function(){
|
||||
this.editingItem = null;
|
||||
@ -343,7 +217,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
this.errors.push("Image cannot start with \"- \"");
|
||||
}
|
||||
|
||||
if (this.editingItem.custom !== "topup" && !this.$refs.txtPrice.checkValidity()) {
|
||||
if (this.editingItem["priceType"] !== "Topup" && !this.$refs.txtPrice.checkValidity()) {
|
||||
this.errors.push("Price must be a valid number");
|
||||
}
|
||||
if (!this.$refs.txtTitle.checkValidity()) {
|
||||
@ -375,7 +249,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}else{
|
||||
this.items.splice(this.editingItem.index,1,this.editingItem);
|
||||
}
|
||||
this.toYml();
|
||||
this.save();
|
||||
this.getModalElement().modal("hide");
|
||||
},
|
||||
escape: function(item) {
|
||||
|
Loading…
Reference in New Issue
Block a user