mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Refactoring: Move AppItem to Client lib and use the class for item list (#6258)
* Refactoring: Move AppItem to Client lib and use the class for item list This makes it available for the app, which would otherwise have to replicate the model. Also uses the proper class for the item/perk list of the app models. * Remove unused app item payment methods property * Do not ignore nullable values in JSON * Revert to use Newtonsoft types
This commit is contained in:
parent
225264a283
commit
ff79a31066
35
BTCPayServer.Client/Models/AppItem.cs
Normal file
35
BTCPayServer.Client/Models/AppItem.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public enum AppItemPriceType
|
||||
{
|
||||
Fixed,
|
||||
Topup,
|
||||
Minimum
|
||||
}
|
||||
|
||||
public class AppItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string[] Categories { get; set; }
|
||||
public string Image { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public AppItemPriceType PriceType { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Price { get; set; }
|
||||
public string BuyButtonText { get; set; }
|
||||
public int? Inventory { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
@ -37,7 +37,7 @@ public abstract class CrowdfundBaseData : AppBaseData
|
||||
|
||||
public class CrowdfundAppData : CrowdfundBaseData
|
||||
{
|
||||
public object? Perks { get; set; }
|
||||
public AppItem[]? Perks { get; set; }
|
||||
}
|
||||
|
||||
public class CrowdfundAppRequest : CrowdfundBaseData, IAppRequest
|
||||
|
@ -1,8 +1,6 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
@ -31,7 +29,7 @@ public abstract class PointOfSaleBaseData : AppBaseData
|
||||
|
||||
public class PointOfSaleAppData : PointOfSaleBaseData
|
||||
{
|
||||
public object? Items { get; set; }
|
||||
public AppItem[]? Items { get; set; }
|
||||
}
|
||||
|
||||
public class PointOfSaleAppRequest : PointOfSaleBaseData, IAppRequest
|
||||
|
@ -3,10 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -25,7 +24,7 @@ using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
using WalletSettingsViewModel = BTCPayServer.Models.StoreViewModels.WalletSettingsViewModel;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -759,39 +758,6 @@ noninventoryitem:
|
||||
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "BTC";
|
||||
vmpos.Template = @"
|
||||
btconly:
|
||||
price: 1.0
|
||||
title: good apple
|
||||
payment_methods:
|
||||
- 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, choiceKey: "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
Assert.Single(btcOnlyInvoice.CryptoInfo);
|
||||
Assert.Equal("BTC",
|
||||
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
|
||||
Assert.Equal("BTC-CHAIN",
|
||||
btcOnlyInvoice.CryptoInfo.First().PaymentType);
|
||||
|
||||
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
|
||||
Assert.Contains(
|
||||
normalInvoice.CryptoInfo,
|
||||
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
|
||||
s.CryptoCode));
|
||||
|
||||
//test topup option
|
||||
vmpos.Template = @"
|
||||
a:
|
||||
@ -821,13 +787,13 @@ g:
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
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.Contains(items, item => item.Id == "a" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == AppItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == AppItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == AppItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == AppItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == AppItemPriceType.Topup);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
|
@ -745,9 +745,9 @@ namespace BTCPayServer.Tests
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
|
||||
var item1 = new ViewPointOfSaleViewModel.Item { Id = "item1", Title = "Item 1", Price = 1, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item2 = new ViewPointOfSaleViewModel.Item { Id = "item2", Title = "Item 2", Price = 2, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item3 = new ViewPointOfSaleViewModel.Item { Id = "item3", Title = "Item 3", Price = 3, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed };
|
||||
var item1 = new AppItem { Id = "item1", Title = "Item 1", Price = 1, PriceType = AppItemPriceType.Fixed };
|
||||
var item2 = new AppItem { Id = "item2", Title = "Item 2", Price = 2, PriceType = AppItemPriceType.Fixed };
|
||||
var item3 = new AppItem { Id = "item3", Title = "Item 3", Price = 3, PriceType = AppItemPriceType.Fixed };
|
||||
var posItems = AppService.SerializeTemplate([item1, item2, item3]);
|
||||
var posApp = await client.CreatePointOfSaleApp(user.StoreId, new PointOfSaleAppRequest { AppName = "test pos", Template = posItems, });
|
||||
var crowdfundApp = await client.CreateCrowdfundApp(user.StoreId, new CrowdfundAppRequest { AppName = "test crowdfund" });
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.UnitTest1;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -76,9 +78,8 @@ fruit tea:
|
||||
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.Equal( AppItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Null( parsedDefault[0].AdditionalData);
|
||||
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||
|
||||
|
||||
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||
@ -87,9 +88,8 @@ fruit tea:
|
||||
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.Equal( AppItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -355,6 +355,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||
Enum.TryParse<PosViewType>(settings.DefaultView.ToString(), true, out var defaultView);
|
||||
var items = AppService.Parse(settings.Template);
|
||||
|
||||
return new PointOfSaleAppData
|
||||
{
|
||||
@ -382,16 +383,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
RedirectUrl = settings.RedirectUrl,
|
||||
Description = settings.Description,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
Items = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
AppService.Parse(settings.Template),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver =
|
||||
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
}
|
||||
)
|
||||
)
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
|
||||
@ -420,6 +412,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
|
||||
var perks = AppService.Parse(settings.PerksTemplate);
|
||||
|
||||
return new CrowdfundAppData
|
||||
{
|
||||
@ -451,15 +444,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
Perks = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
AppService.Parse(settings.PerksTemplate),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
}
|
||||
)
|
||||
)
|
||||
Perks = perks
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,6 @@ using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -290,7 +289,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item[] items;
|
||||
AppItem[] items;
|
||||
string currencyCode;
|
||||
PointOfSaleSettings posS = null;
|
||||
switch (app.AppType)
|
||||
@ -310,7 +309,7 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ViewPointOfSaleViewModel.Item item = null;
|
||||
AppItem item = null;
|
||||
if (!string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
|
||||
@ -321,13 +320,8 @@ namespace BTCPayServer
|
||||
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
item1.Id.Equals(escapedItemId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (item is null ||
|
||||
item.Inventory <= 0 ||
|
||||
(item.PaymentMethods?.Any() is true &&
|
||||
item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false))
|
||||
{
|
||||
if (item is null || item.Inventory <= 0)
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
else if (app.AppType == PointOfSaleAppType.AppType && posS?.ShowCustomAmount is not true)
|
||||
{
|
||||
@ -336,7 +330,7 @@ namespace BTCPayServer
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
|
||||
Amount = item?.PriceType == AppItemPriceType.Topup ? null : item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
@ -350,7 +344,7 @@ namespace BTCPayServer
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
var allowOverpay = item?.PriceType is not AppItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
@ -12,11 +13,9 @@ using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -398,9 +397,10 @@ namespace BTCPayServer.Hosting
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] ParsePOSYML(string yaml)
|
||||
|
||||
public static AppItem[] ParsePOSYML(string yaml)
|
||||
{
|
||||
var items = new List<ViewPointOfSaleViewModel.Item>();
|
||||
var items = new List<AppItem>();
|
||||
var stream = new YamlStream();
|
||||
if (string.IsNullOrEmpty(yaml))
|
||||
return items.ToArray();
|
||||
@ -417,11 +417,11 @@ namespace BTCPayServer.Hosting
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentItem = new ViewPointOfSaleViewModel.Item
|
||||
var currentItem = new AppItem
|
||||
{
|
||||
Id = trimmedKey,
|
||||
Title = trimmedKey,
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed
|
||||
PriceType = AppItemPriceType.Fixed
|
||||
};
|
||||
var itemSpecs = (YamlMappingNode)posItem.Value;
|
||||
foreach (var spec in itemSpecs)
|
||||
@ -446,12 +446,6 @@ namespace BTCPayServer.Hosting
|
||||
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))
|
||||
@ -459,15 +453,15 @@ namespace BTCPayServer.Hosting
|
||||
if (customBoolValue)
|
||||
{
|
||||
currentItem.PriceType = currentItem.Price is null or 0
|
||||
? ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
: ViewPointOfSaleViewModel.ItemPriceType.Minimum;
|
||||
? AppItemPriceType.Topup
|
||||
: AppItemPriceType.Minimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentItem.PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
currentItem.PriceType = AppItemPriceType.Fixed;
|
||||
}
|
||||
}
|
||||
else if (Enum.TryParse<ViewPointOfSaleViewModel.ItemPriceType>(scalarValue?.Value, true,
|
||||
else if (Enum.TryParse<AppItemPriceType>(scalarValue?.Value, true,
|
||||
out var customPriceType))
|
||||
{
|
||||
currentItem.PriceType = customPriceType;
|
||||
|
@ -17,7 +17,6 @@ using BTCPayServer.Forms;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -149,36 +148,28 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
decimal? price = request.Amount;
|
||||
var title = settings.Title;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = AppService.Parse(settings.PerksTemplate, false);
|
||||
choice = choices?.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
AppItem choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
|
||||
if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
if (choice.PriceType == AppItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = choice.Price.Value;
|
||||
if (choice.Price.HasValue)
|
||||
price = choice.Price.Value;
|
||||
if (request.Amount > price)
|
||||
price = request.Amount;
|
||||
}
|
||||
if (choice.Inventory.HasValue)
|
||||
if (choice.Inventory is <= 0)
|
||||
{
|
||||
if (choice.Inventory <= 0)
|
||||
{
|
||||
return NotFound("Option was out of stock");
|
||||
}
|
||||
}
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
|
||||
return NotFound("Option was out of stock");
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -231,8 +222,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
@ -26,7 +26,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
public CrowdfundInfo Info { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public StoreBrandingViewModel StoreBranding { get; set; }
|
||||
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
|
||||
public AppItem[] Perks { get; set; }
|
||||
public bool SimpleDisplay { get; set; }
|
||||
public bool DisqusEnabled { get; set; }
|
||||
public bool SoundsEnabled { get; set; }
|
||||
|
@ -176,9 +176,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
string title;
|
||||
decimal? price;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
AppItem choice = null;
|
||||
List<PosCartItem> cartItems = null;
|
||||
ViewPointOfSaleViewModel.Item[] choices = null;
|
||||
AppItem[] choices = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
@ -186,7 +186,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
if (choice.PriceType == AppItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
@ -201,12 +201,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -239,7 +233,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
var expectedCartItemPrice = itemChoice.PriceType != AppItemPriceType.Topup
|
||||
? itemChoice.Price ?? 0
|
||||
: 0;
|
||||
|
||||
@ -373,7 +367,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var ident = selectedChoice.Title ?? selectedChoice.Id;
|
||||
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
var key = selectedChoice.PriceType == AppItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
|
||||
|
@ -2,52 +2,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
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
|
||||
{
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Description { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string[] Categories { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Image { 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
|
||||
{
|
||||
public bool Prefixed { get; set; }
|
||||
@ -70,8 +32,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public bool EnableTips { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
Item[] _Items;
|
||||
public Item[] Items
|
||||
AppItem[] _Items;
|
||||
public AppItem[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -10,7 +10,6 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -95,7 +94,7 @@ namespace BTCPayServer.Services.Apps
|
||||
return await salesType.GetItemStats(appData, paidInvoices);
|
||||
}
|
||||
|
||||
public static Task<AppSalesStats> GetSalesStatswithPOSItems(ViewPointOfSaleViewModel.Item[] items,
|
||||
public static Task<AppSalesStats> GetSalesStatswithPOSItems(AppItem[] items,
|
||||
InvoiceEntity[] paidInvoices, int numberOfDays)
|
||||
{
|
||||
var series = paidInvoices
|
||||
@ -145,7 +144,7 @@ namespace BTCPayServer.Services.Apps
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
|
||||
public static Func<List<InvoiceStatsItem>, InvoiceEntity, List<InvoiceStatsItem>> AggregateInvoiceEntitiesForStats(ViewPointOfSaleViewModel.Item[] items)
|
||||
public static Func<List<InvoiceStatsItem>, InvoiceEntity, List<InvoiceStatsItem>> AggregateInvoiceEntitiesForStats(AppItem[] items)
|
||||
{
|
||||
return (res, e) =>
|
||||
{
|
||||
@ -344,14 +343,14 @@ namespace BTCPayServer.Services.Apps
|
||||
return _storeRepository.FindStore(app.StoreDataId);
|
||||
}
|
||||
|
||||
public static string SerializeTemplate(ViewPointOfSaleViewModel.Item[] items)
|
||||
public static string SerializeTemplate(AppItem[] items)
|
||||
{
|
||||
return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer);
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] Parse(string template, bool includeDisabled = true, bool throws = false)
|
||||
public static AppItem[] Parse(string template, bool includeDisabled = true, bool throws = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template)) return [];
|
||||
var allItems = JsonConvert.DeserializeObject<ViewPointOfSaleViewModel.Item[]>(template, _defaultSerializer)!;
|
||||
var allItems = JsonConvert.DeserializeObject<AppItem[]>(template, _defaultSerializer)!;
|
||||
// ensure all items have an id, which is also unique
|
||||
var itemsWithoutId = allItems.Where(i => string.IsNullOrEmpty(i.Id)).ToList();
|
||||
if (itemsWithoutId.Any() && throws) throw new ArgumentException($"Missing ID for item \"{itemsWithoutId.First().Title}\".");
|
||||
|
@ -1,4 +1,4 @@
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
@ -8,71 +8,70 @@ namespace BTCPayServer.Services.Apps
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "Tea shop";
|
||||
Template = AppService.SerializeTemplate(new ViewPointOfSaleViewModel.Item[]
|
||||
{
|
||||
new()
|
||||
Template = AppService.SerializeTemplate([
|
||||
new AppItem
|
||||
{
|
||||
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,
|
||||
PriceType = AppItemPriceType.Fixed,
|
||||
Price = 1
|
||||
},
|
||||
new()
|
||||
new AppItem
|
||||
{
|
||||
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,
|
||||
PriceType = AppItemPriceType.Fixed,
|
||||
Price = 1
|
||||
},
|
||||
new()
|
||||
new AppItem
|
||||
{
|
||||
Id = "rooibos",
|
||||
Title = "Rooibos (limited)",
|
||||
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,
|
||||
PriceType = AppItemPriceType.Fixed,
|
||||
Price = 1.2m,
|
||||
Inventory = 5,
|
||||
},
|
||||
new()
|
||||
new AppItem
|
||||
{
|
||||
Id = "pu-erh",
|
||||
Title = "Pu Erh (free)",
|
||||
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,
|
||||
PriceType = AppItemPriceType.Fixed,
|
||||
Price = 0
|
||||
},
|
||||
new()
|
||||
new AppItem
|
||||
{
|
||||
Id = "herbal-tea",
|
||||
Title = "Herbal Tea (minimum)",
|
||||
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,
|
||||
PriceType = AppItemPriceType.Minimum,
|
||||
Price = 1.8m,
|
||||
Disabled = false
|
||||
},
|
||||
new()
|
||||
new AppItem
|
||||
{
|
||||
Id = "fruit-tea",
|
||||
Title = "Fruit Tea (any amount)",
|
||||
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,
|
||||
PriceType = AppItemPriceType.Topup,
|
||||
Disabled = false
|
||||
}
|
||||
});
|
||||
]);
|
||||
DefaultView = PosViewType.Static;
|
||||
ShowCustomAmount = false;
|
||||
ShowDiscount = false;
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Client.Models
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.Crowdfund.Models.ContributeToCrowdfund
|
||||
|
||||
@{ var vm = Model.ViewCrowdfundViewModel; }
|
||||
@ -32,16 +33,16 @@
|
||||
<span>@item.Price.Value</span>
|
||||
<span>@vm.TargetCurrency</span>
|
||||
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum)
|
||||
if (item.PriceType == AppItemPriceType.Minimum)
|
||||
{
|
||||
@Safe.Raw(StringLocalizer["or more"])
|
||||
}
|
||||
}
|
||||
else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup )
|
||||
else if (item.PriceType == AppItemPriceType.Topup)
|
||||
{
|
||||
@Safe.Raw(StringLocalizer["Any amount"])
|
||||
}
|
||||
else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed)
|
||||
else if (item.PriceType == AppItemPriceType.Fixed)
|
||||
{
|
||||
@Safe.Raw(StringLocalizer["Free"])
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json.Linq
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@ -21,12 +21,12 @@
|
||||
<script src="~/pos/cart.js" asp-append-version="true"></script>
|
||||
}
|
||||
@functions {
|
||||
private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item)
|
||||
private string GetItemPriceFormatted(AppItem item)
|
||||
{
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount";
|
||||
if (item.PriceType == AppItemPriceType.Topup) return "any amount";
|
||||
if (item.Price == 0) return "free";
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
return item.PriceType == AppItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@
|
||||
var formatted = GetItemPriceFormatted(item);
|
||||
var inStock = item.Inventory is null or > 0;
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText)
|
||||
? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
|
||||
? item.PriceType == AppItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
|
||||
: item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
|
||||
var categories = new JArray(item.Categories ?? new object[] { });
|
||||
@ -86,7 +86,7 @@
|
||||
<div class="card-body d-flex flex-column gap-2 mb-auto">
|
||||
<h5 class="card-title m-0">@Safe.Raw(item.Title)</h5>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
@if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0)
|
||||
@if (item.PriceType == AppItemPriceType.Topup || item.Price == 0)
|
||||
{
|
||||
<span class="fw-semibold badge text-bg-info">@Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..])</span>
|
||||
}
|
||||
@ -116,7 +116,7 @@
|
||||
@if (inStock)
|
||||
{
|
||||
<form class="card-footer">
|
||||
@if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed)
|
||||
@if (item.PriceType != AppItemPriceType.Fixed)
|
||||
{
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
|
@ -1,15 +1,17 @@
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Payments.Lightning
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using BTCPayServer.Services.Stores
|
||||
@using LNURL
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject StoreRepository StoreRepository
|
||||
@inject PaymentMethodHandlerDictionary Handlers
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@{
|
||||
var store = await StoreRepository.FindStore(Model.StoreId);
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
@ -58,16 +60,15 @@ else
|
||||
{
|
||||
if (Model.ShowCustomAmount)
|
||||
{
|
||||
Model.Items = Model.Items.Concat(new[]
|
||||
{
|
||||
new ViewPointOfSaleViewModel.Item()
|
||||
Model.Items = Model.Items.Concat([
|
||||
new AppItem
|
||||
{
|
||||
Description = "Create invoice to pay custom amount",
|
||||
Title = "Custom amount",
|
||||
BuyButtonText = Model.CustomButtonText,
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
PriceType = AppItemPriceType.Topup
|
||||
}
|
||||
}).ToArray();
|
||||
]).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +77,7 @@ else
|
||||
{
|
||||
var item = Model.Items[x];
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && item.Price == 0) continue;
|
||||
if (item.PriceType == AppItemPriceType.Fixed && item.Price == 0) continue;
|
||||
<div class="d-flex flex-wrap">
|
||||
<div class="tile card w-100" data-id="@x">
|
||||
<div class="card-body pt-0 d-flex flex-column gap-2">
|
||||
@ -85,13 +86,13 @@ else
|
||||
<span class="fw-semibold">
|
||||
@switch (item.PriceType)
|
||||
{
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Topup:
|
||||
case AppItemPriceType.Topup:
|
||||
<span>Any amount</span>
|
||||
break;
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Minimum:
|
||||
case AppItemPriceType.Minimum:
|
||||
<span>@formatted minimum</span>
|
||||
break;
|
||||
case ViewPointOfSaleViewModel.ItemPriceType.Fixed:
|
||||
case AppItemPriceType.Fixed:
|
||||
@formatted
|
||||
break;
|
||||
default:
|
||||
|
@ -1,18 +1,18 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
}
|
||||
@functions {
|
||||
private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item)
|
||||
private string GetItemPriceFormatted(AppItem item)
|
||||
{
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount";
|
||||
if (item.PriceType == AppItemPriceType.Topup) return "any amount";
|
||||
if (item.Price == 0) return "free";
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
return item.PriceType == AppItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
var formatted = GetItemPriceFormatted(item);
|
||||
var inStock = item.Inventory is null or > 0;
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText)
|
||||
? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
|
||||
? item.PriceType == AppItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
|
||||
: item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
<div class="card-body d-flex flex-column gap-2 mb-auto">
|
||||
<h5 class="card-title m-0">@Safe.Raw(item.Title)</h5>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
@if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0)
|
||||
@if (item.PriceType == AppItemPriceType.Topup || item.Price == 0)
|
||||
{
|
||||
<span class="fw-semibold badge text-bg-info">@Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..])</span>
|
||||
}
|
||||
@ -76,7 +76,7 @@
|
||||
{
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" autocomplete="off">
|
||||
<input type="hidden" name="choiceKey" value="@item.Id" />
|
||||
@if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum)
|
||||
@if (item.PriceType == AppItemPriceType.Minimum)
|
||||
{
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
|
@ -3,15 +3,16 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json.Linq
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@functions {
|
||||
private string GetItemPriceFormatted(ViewPointOfSaleViewModel.Item item)
|
||||
private string GetItemPriceFormatted(AppItem item)
|
||||
{
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup) return "any amount";
|
||||
if (item.PriceType == AppItemPriceType.Topup) return "any amount";
|
||||
if (item.Price == 0) return "free";
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
return item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
return item.PriceType == AppItemPriceType.Minimum ? $"{formatted} minimum" : formatted;
|
||||
}
|
||||
}
|
||||
<form id="app" method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 my-auto" v-cloak>
|
||||
@ -115,7 +116,7 @@
|
||||
var item = Model.Items[index];
|
||||
var formatted = GetItemPriceFormatted(item);
|
||||
var inStock = item.Inventory is null or > 0;
|
||||
var displayed = item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed && inStock ? "true" : "false";
|
||||
var displayed = item.PriceType == AppItemPriceType.Fixed && inStock ? "true" : "false";
|
||||
var categories = new JArray(item.Categories ?? new object[] { });
|
||||
<div class="posItem p-3" :class="{ 'posItem--inStock': inStock(@index), 'posItem--displayed': @displayed }" data-index="@index" data-search="@Safe.RawEncode(item.Title + " " + item.Description)" data-categories='@Safe.Json(categories)' v-show="@displayed">
|
||||
<div class="d-flex align-items-start w-100 gap-3">
|
||||
@ -128,7 +129,7 @@
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<h5 class="card-title m-0">@Safe.Raw(item.Title)</h5>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
@if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup || item.Price == 0)
|
||||
@if (item.PriceType == AppItemPriceType.Topup || item.Price == 0)
|
||||
{
|
||||
<span class="fw-semibold badge text-bg-info">@Safe.Raw(char.ToUpper(formatted[0]) + formatted[1..])</span>
|
||||
}
|
||||
|
@ -747,34 +747,31 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AppItem"
|
||||
},
|
||||
"description": "JSON object of app items",
|
||||
"example": [
|
||||
{
|
||||
"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.",
|
||||
"id": "green tea",
|
||||
"image": "~/img/pos-sample/green-tea.jpg",
|
||||
"price": {
|
||||
"type": 2,
|
||||
"formatted": "$1.00",
|
||||
"value": 1.0
|
||||
},
|
||||
"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",
|
||||
"price": "1.0",
|
||||
"priceType": "Fixed",
|
||||
"buyButtonText": null,
|
||||
"inventory": 5,
|
||||
"paymentMethods": null,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"id": "black tea",
|
||||
"image": "~/img/pos-sample/black-tea.jpg",
|
||||
"price": {
|
||||
"type": 2,
|
||||
"formatted": "$1.00",
|
||||
"value": 1.0
|
||||
},
|
||||
"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",
|
||||
"price": "2.0",
|
||||
"priceType": "Fixed",
|
||||
"buyButtonText": "Test Buy Button Text",
|
||||
"inventory": null,
|
||||
"paymentMethods": null,
|
||||
@ -1012,6 +1009,66 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AppItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "green-tea",
|
||||
"description": "Unique ID of the item"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Green Tea",
|
||||
"description": "The display name of the item"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Lovely, fresh and tender.",
|
||||
"description": "A description text for the item"
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": "http://teashop.com/img/green-tea.jpg",
|
||||
"description": "An image URL for the item"
|
||||
},
|
||||
"price": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"nullable": true,
|
||||
"example": "21.0"
|
||||
},
|
||||
"priceType": {
|
||||
"type": "string",
|
||||
"x-enumNames": [
|
||||
"Fixed",
|
||||
"Topup",
|
||||
"Minimum"
|
||||
],
|
||||
"enum": [
|
||||
"Fixed",
|
||||
"Topup",
|
||||
"Minimum"
|
||||
]
|
||||
},
|
||||
"buyButtonText": {
|
||||
"type": "string",
|
||||
"example": "Buy me!",
|
||||
"description": "A custom text for the buy button for the item"
|
||||
},
|
||||
"inventory": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"example": 21,
|
||||
"description": "The remaining stock the item"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"description": "If true, the item does not appear in the list by default.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppSalesStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
Loading…
Reference in New Issue
Block a user