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

View File

@@ -6,14 +6,18 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Fido2; using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models; using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using ExchangeSharp;
using Fido2NetLib.Objects; using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -31,6 +35,7 @@ namespace BTCPayServer.Hosting
private readonly StoreRepository _StoreRepository; private readonly StoreRepository _StoreRepository;
private readonly BTCPayNetworkProvider _NetworkProvider; private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly SettingsRepository _Settings; private readonly SettingsRepository _Settings;
private readonly AppService _appService;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
public IOptions<LightningNetworkOptions> LightningOptions { get; } public IOptions<LightningNetworkOptions> LightningOptions { get; }
@@ -41,12 +46,14 @@ namespace BTCPayServer.Hosting
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IOptions<LightningNetworkOptions> lightningOptions, IOptions<LightningNetworkOptions> lightningOptions,
SettingsRepository settingsRepository) SettingsRepository settingsRepository,
AppService appService)
{ {
_DBContextFactory = dbContextFactory; _DBContextFactory = dbContextFactory;
_StoreRepository = storeRepository; _StoreRepository = storeRepository;
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
_Settings = settingsRepository; _Settings = settingsRepository;
_appService = appService;
_userManager = userManager; _userManager = userManager;
LightningOptions = lightningOptions; LightningOptions = lightningOptions;
} }
@@ -134,6 +141,12 @@ namespace BTCPayServer.Hosting
settings.MigrateHotwalletProperty = true; settings.MigrateHotwalletProperty = true;
await _Settings.UpdateSetting(settings); await _Settings.UpdateSetting(settings);
} }
if (!settings.MigrateAppCustomOption)
{
await MigrateAppCustomOption();
settings.MigrateAppCustomOption = true;
await _Settings.UpdateSetting(settings);
}
} }
catch (Exception ex) 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() private async Task MigrateHotwalletProperty()
{ {
await using var ctx = _DBContextFactory.CreateContext(); await using var ctx = _DBContextFactory.CreateContext();

View File

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

View File

@@ -289,7 +289,7 @@ namespace BTCPayServer.Services.Apps
{ {
var itemNode = new YamlMappingNode(); var itemNode = new YamlMappingNode();
itemNode.Add("title", new YamlScalarNode(item.Title)); 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())); itemNode.Add("price", new YamlScalarNode(item.Price.Value.ToStringInvariant()));
if (!string.IsNullOrEmpty(item.Description)) if (!string.IsNullOrEmpty(item.Description))
{ {
@@ -302,7 +302,7 @@ namespace BTCPayServer.Services.Apps
{ {
itemNode.Add("image", new YamlScalarNode(item.Image)); 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())); itemNode.Add("disabled", new YamlScalarNode(item.Disabled.ToStringLowerInvariant()));
if (item.Inventory.HasValue) if (item.Inventory.HasValue)
{ {
@@ -336,26 +336,50 @@ namespace BTCPayServer.Services.Apps
.Children .Children
.Select(kv => new PosHolder(_HtmlSanitizer) { Key = _HtmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode }) .Select(kv => new PosHolder(_HtmlSanitizer) { Key = _HtmlSanitizer.Sanitize((kv.Key as YamlScalarNode)?.Value), Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null) .Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item() .Select(c =>
{
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"), Description = c.GetDetailString("description"),
Id = c.Key, Id = c.Key,
Image = c.GetDetailString("image"), Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key, Title = c.GetDetailString("title") ?? c.Key,
Custom = c.GetDetailString("custom"), Price = price,
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"), BuyButtonText = c.GetDetailString("buyButtonText"),
Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ? (int?)null : int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture), Inventory =
string.IsNullOrEmpty(c.GetDetailString("inventory"))
? (int?)null
: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
PaymentMethods = c.GetDetailStringList("payment_methods"), PaymentMethods = c.GetDetailStringList("payment_methods"),
Disabled = c.GetDetailString("disabled") == "true" Disabled = c.GetDetailString("disabled") == "true"
};
}) })
.ToArray(); .ToArray();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded",function (ev) {
}, },
computed: { computed: {
canExpand: function(){ 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: { methods: {
@@ -45,14 +45,14 @@ document.addEventListener("DOMContentLoaded",function (ev) {
} }
}, },
setAmount: function (amount) { 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; this.expanded = false;
} }
}, },
mounted: function () { mounted: function () {
this.setAmount(this.perk.price?.value); this.setAmount(this.perk.price.value);
}, },
watch: { watch: {
perk: function (newValue, oldValue) { perk: function (newValue, oldValue) {