use more concrete types for price type in app items

This commit is contained in:
Kukks 2021-10-11 12:46:05 +02:00 committed by Andrew Camilleri
parent 33a893ba31
commit 9592a77cff
11 changed files with 135 additions and 53 deletions

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
if (choice == null)
return NotFound();
title = choice.Title;
if (choice.Custom == "topup")
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
@ -323,7 +323,7 @@ namespace BTCPayServer.Controllers
return NotFound("Incorrect option provided");
title = choice.Title;
if (choice.Custom == "topup")
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}

View File

@ -6,14 +6,18 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using ExchangeSharp;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
@ -31,6 +35,7 @@ namespace BTCPayServer.Hosting
private readonly StoreRepository _StoreRepository;
private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly SettingsRepository _Settings;
private readonly AppService _appService;
private readonly UserManager<ApplicationUser> _userManager;
public IOptions<LightningNetworkOptions> LightningOptions { get; }
@ -41,12 +46,14 @@ namespace BTCPayServer.Hosting
ApplicationDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager,
IOptions<LightningNetworkOptions> lightningOptions,
SettingsRepository settingsRepository)
SettingsRepository settingsRepository,
AppService appService)
{
_DBContextFactory = dbContextFactory;
_StoreRepository = storeRepository;
_NetworkProvider = networkProvider;
_Settings = settingsRepository;
_appService = appService;
_userManager = userManager;
LightningOptions = lightningOptions;
}
@ -134,6 +141,12 @@ namespace BTCPayServer.Hosting
settings.MigrateHotwalletProperty = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.MigrateAppCustomOption)
{
await MigrateAppCustomOption();
settings.MigrateAppCustomOption = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -142,6 +155,42 @@ namespace BTCPayServer.Hosting
}
}
private async Task MigrateAppCustomOption()
{
await using var ctx = _DBContextFactory.CreateContext();
foreach (var app in await ctx.Apps.AsQueryable().ToArrayAsync())
{
ViewPointOfSaleViewModel.Item[] items;
string newTemplate;
switch (app.AppType)
{
case nameof(AppType.Crowdfund):
var settings1 = app.GetSettings<CrowdfundSettings>();
items = _appService.Parse(settings1.PerksTemplate, settings1.TargetCurrency);
newTemplate = _appService.SerializeTemplate(items);
if (settings1.PerksTemplate != newTemplate)
{
settings1.PerksTemplate = newTemplate;
app.SetSettings(settings1);
};
break;
case nameof(AppType.PointOfSale):
var settings2 = app.GetSettings<AppsController.PointOfSaleSettings>();
items = _appService.Parse(settings2.Template, settings2.Currency);
newTemplate = _appService.SerializeTemplate(items);
if (settings2.Template != newTemplate)
{
settings2.Template = newTemplate;
app.SetSettings(settings2);
};
break;
}
}
await ctx.SaveChangesAsync();
}
private async Task MigrateHotwalletProperty()
{
await using var ctx = _DBContextFactory.CreateContext();

View File

@ -9,15 +9,22 @@ namespace BTCPayServer.Models.AppViewModels
{
public class ItemPrice
{
public enum ItemPriceType
{
Topup,
Minimum,
Fixed
}
public ItemPriceType Type { get; set; }
public string Formatted { get; set; }
public decimal Value { get; set; }
public decimal? Value { get; set; }
}
public string Description { get; set; }
public string Id { get; set; }
public string Image { get; set; }
public ItemPrice Price { get; set; }
public string Title { get; set; }
public string Custom { get; set; }
public string BuyButtonText { get; set; }
public int? Inventory { get; set; } = null;
public string[] PaymentMethods { get; set; }

View File

@ -289,7 +289,7 @@ namespace BTCPayServer.Services.Apps
{
var itemNode = new YamlMappingNode();
itemNode.Add("title", new YamlScalarNode(item.Title));
if(item.Custom!= "topup")
if(item.Price.Type!= ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
if (!string.IsNullOrEmpty(item.Description))
{
@ -302,7 +302,7 @@ namespace BTCPayServer.Services.Apps
{
itemNode.Add("image", new YamlScalarNode(item.Image));
}
itemNode.Add("custom", new YamlScalarNode(item.Custom.ToStringLowerInvariant()));
itemNode.Add("price_type", new YamlScalarNode(item.Price.Type.ToStringLowerInvariant()));
itemNode.Add("disabled", new YamlScalarNode(item.Disabled.ToStringLowerInvariant()));
if (item.Inventory.HasValue)
{
@ -336,26 +336,50 @@ namespace BTCPayServer.Services.Apps
.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 => new ViewPointOfSaleViewModel.Item()
.Select(c =>
{
Description = c.GetDetailString("description"),
Id = c.Key,
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Custom = c.GetDetailString("custom"),
Price =
c.GetDetailString("custom") == "topup"
? null
: c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency)
}).Single(),
BuyButtonText = c.GetDetailString("buyButtonText"),
Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ? (int?)null : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
PaymentMethods = c.GetDetailStringList("payment_methods"),
Disabled = c.GetDetailString("disabled") == "true"
ViewPointOfSaleViewModel.Item.ItemPrice price = new ViewPointOfSaleViewModel.Item.ItemPrice();
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)
{
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency);
}
break;
case "fixed":
case "false":
case null:
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency);
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"))
? (int?)null
: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
PaymentMethods = c.GetDetailStringList("payment_methods"),
Disabled = c.GetDetailString("disabled") == "true"
};
})
.ToArray();
}

View File

@ -25,5 +25,6 @@ namespace BTCPayServer.Services
// Done in DbMigrationsHostedService
public int? MigratedInvoiceTextSearchPages { get; set; }
public bool MigrateAppCustomOption { get; set; }
}
}

View File

@ -64,7 +64,8 @@
</select>
</div>
<div class="col-sm-3" v-show="editingItem.custom !== 'topup'">
<label class="form-label" data-required>Price</label>
<label class="form-label">&nbsp</label>
<input class="form-control mb-2"
inputmode="numeric"
pattern="\d*"
@ -121,8 +122,8 @@ document.addEventListener("DOMContentLoaded", function () {
items: [],
editingItem: null,
customPriceOptions: [
{ text: 'Fixed', value: false },
{ text: 'Minimum', value: true },
{ text: 'Fixed', value: "fixed" },
{ text: 'Minimum', value: "minimum" },
{ text: 'Topup', value: 'topup' },
],
elementId: "@Model.templateId"
@ -207,8 +208,8 @@ document.addEventListener("DOMContentLoaded", function () {
if (productProperty.indexOf('image:') !== -1) {
image = productProperty.replace('image:', '').trim();
}
if (productProperty.indexOf('custom:') !== -1) {
custom = productProperty.replace('custom:', '').trim();
if (productProperty.indexOf('price_type:') !== -1) {
custom = productProperty.replace('price_type:', '').trim();
}
if (productProperty.indexOf('buyButtonText:') !== -1) {
buyButtonText = productProperty.replace('buyButtonText:', '').trim();
@ -232,7 +233,7 @@ document.addEventListener("DOMContentLoaded", function () {
price: price,
image: image || null,
description: description || '',
custom: custom === "topup"? "topup": custom === "true",
custom: custom,
buyButtonText: buyButtonText,
inventory: isNaN(inventory)? null: inventory,
paymentMethods: paymentMethods,
@ -271,7 +272,7 @@ document.addEventListener("DOMContentLoaded", function () {
itemTemplate += ' inventory: ' + inventory + '\n';
}
if (custom != null) {
itemTemplate += ' custom: ' + (custom === "topup"? '"topup"': custom) + '\n';
itemTemplate += ' price_type: "' + custom + '" \n';
}
if (buyButtonText != null && buyButtonText.length > 0) {
itemTemplate += ' buyButtonText: ' + buyButtonText + '\n';
@ -293,7 +294,7 @@ document.addEventListener("DOMContentLoaded", function () {
editItem: function(index){
this.errors = [];
if(index < 0){
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: false, inventory: null, paymentMethods: [], disabled: false};
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: "fixed", inventory: null, paymentMethods: [], disabled: false};
}else{
this.editingItem = {...this.items[index], index};
}

View File

@ -1,3 +1,4 @@
@using BTCPayServer.Models.AppViewModels
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
@{ var vm = Model.ViewCrowdfundViewModel; }
@ -26,20 +27,20 @@
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
</label>
<span class="text-muted">
@if (item.Price?.Value > 0)
@if (item.Price.Value > 0)
{
<span>@item.Price.Value</span>
<span>@vm.TargetCurrency</span>
if (item.Custom == "true")
if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum)
{
@Safe.Raw("or more")
}
}
else if (item.Custom == "topup" || item.Custom == "true" )
else if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed )
{
@Safe.Raw("Any amount")
}else if (item.Custom == "false")
}else if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed)
{
@Safe.Raw("Free")
}
@ -54,7 +55,7 @@
{
case null:
break;
case int i when i <= 0:
case var i when i <= 0:
<span>Sold out</span>
break;
default:

View File

@ -1,3 +1,4 @@
@using BTCPayServer.Models.AppViewModels
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
Layout = "_LayoutPos";
@ -232,8 +233,8 @@
<span class="text-muted small">
@{
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? (item.Custom == "true" || item.Custom == "topup") ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
if (item.Custom != "topup")
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)
{
buttonText = buttonText.Replace("{0}",item.Price.Formatted)
?.Replace("{Price}",item.Price.Formatted);

View File

@ -1,3 +1,4 @@
@using BTCPayServer.Models.AppViewModels
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
Layout = "_LayoutPos";
@ -19,12 +20,9 @@
@for (int x = 0; x < Model.Items.Length; x++)
{
var item = Model.Items[x];
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? (item.Custom == "true" || item.Custom == "topup") ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
if (item.Custom != "topup")
{
buttonText = buttonText.Replace("{0}",item.Price.Formatted)
?.Replace("{Price}",item.Price.Formatted);
}
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);
<div class="card px-0" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image))
@ -35,7 +33,7 @@
<div class="card-footer bg-transparent border-0 pb-3">
@if (!item.Inventory.HasValue || item.Inventory.Value > 0)
{
@if (item.Custom == "true")
@if (item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed)
{
<form method="post" asp-controller="AppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>

View File

@ -112,7 +112,7 @@ Cart.prototype.getTotalProducts = function() {
typeof this.content[key] != 'undefined' &&
!this.content[key].disabled
) {
var price = this.toCents(this.content[key].price?.value??0);
const price = this.toCents(this.content[key].price.value ||0);
amount += (this.content[key].count * price);
}
}
@ -439,7 +439,7 @@ Cart.prototype.listItems = function() {
'title': this.escape(item.title),
'count': this.escape(item.count),
'inventory': this.escape(item.inventory < 0? 99999: item.inventory),
'price': this.escape(item.price?.formatted??0)
'price': this.escape(item.price.formatted || 0)
});
list.push($(tableTemplate));
}

View File

@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded",function (ev) {
},
computed: {
canExpand: function(){
return !this.expanded && this.active && (this.perk.custom=="topup" || this.perk.price.value || this.perk.custom == "true") && (this.perk.inventory==null || this.perk.inventory > 0)
return !this.expanded && this.active && (this.perk.custom !== "fixed" || this.perk.price.value) && (this.perk.inventory==null || this.perk.inventory > 0)
}
},
methods: {
@ -45,14 +45,14 @@ document.addEventListener("DOMContentLoaded",function (ev) {
}
},
setAmount: function (amount) {
this.amount = this.perk.custom == "topup"? null : (amount || 0).noExponents();
this.amount = this.perk.custom === "topup"? null : (amount || 0).noExponents();
this.expanded = false;
}
},
mounted: function () {
this.setAmount(this.perk.price?.value);
this.setAmount(this.perk.price.value);
},
watch: {
perk: function (newValue, oldValue) {