POS: Handle flexible price items in cart view (#5238)

This commit is contained in:
d11n 2023-08-09 10:31:19 +03:00 committed by GitHub
parent 19d360a543
commit d67ebd957e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 68 deletions

View file

@ -663,7 +663,7 @@ donation:
Assert.Equal(3, vmview.Items.Length); Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title); Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title); Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value); Assert.Equal(10.0m, vmview.Items[1].Price);
Assert.Equal("{0} Purchase", vmview.ButtonText); Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText); Assert.Equal("Wanna tip?", vmview.CustomTipText);
@ -680,7 +680,7 @@ donation:
Assert.IsType<RedirectToActionResult>(publicApps Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
invoices = user.BitPay.GetInvoices(); invoices = await user.BitPay.GetInvoicesAsync();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice); Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc); Assert.Equal("good apple", appleInvoice.ItemDesc);
@ -689,7 +689,7 @@ donation:
var action = Assert.IsType<RedirectToActionResult>(publicApps var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result); .ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName); Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices(); invoices = await user.BitPay.GetInvoicesAsync();
var donationInvoice = invoices.Single(i => i.Price == 6.6m); var donationInvoice = invoices.Single(i => i.Price == 6.6m);
Assert.NotNull(donationInvoice); Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency); Assert.Equal("CAD", donationInvoice.Currency);

View file

@ -2064,7 +2064,6 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester(); using var s = CreateSeleniumTester();
s.Server.ActivateLightning(); s.Server.ActivateLightning();
await s.StartAsync(); await s.StartAsync();
await s.Server.EnsureChannelsSetup(); await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true); s.RegisterNewUser(true);
@ -2101,7 +2100,6 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester(); using var s = CreateSeleniumTester();
s.Server.ActivateLightning(); s.Server.ActivateLightning();
await s.StartAsync(); await s.StartAsync();
await s.Server.EnsureChannelsSetup(); await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true); s.RegisterNewUser(true);
@ -2176,7 +2174,6 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester(); using var s = CreateSeleniumTester();
s.Server.ActivateLightning(); s.Server.ActivateLightning();
await s.StartAsync(); await s.StartAsync();
await s.Server.EnsureChannelsSetup(); await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true); s.RegisterNewUser(true);
@ -2199,6 +2196,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(windows[1]); s.Driver.SwitchTo().Window(windows[1]);
s.Driver.WaitForElement(By.Id("PosItems")); s.Driver.WaitForElement(By.Id("PosItems"));
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr"))); Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
var posUrl = s.Driver.Url;
// Select and clear // Select and clear
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click(); s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
@ -2207,34 +2205,81 @@ namespace BTCPayServer.Tests
Thread.Sleep(250); Thread.Sleep(250);
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr"))); Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
// Select items // Select simple items
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
Thread.Sleep(250);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click(); s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
Thread.Sleep(250); Thread.Sleep(250);
Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
Thread.Sleep(250);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click(); s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
Thread.Sleep(250); Thread.Sleep(250);
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count); Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text); Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select item with inventory - two of it
Assert.Equal("5 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
Thread.Sleep(250);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(3, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("5,40 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with minimum amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(4, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("7,20 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with adjusted minimum amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).SendKeys("2.3");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(5, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("9,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with custom amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".2");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(6, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("9,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with another custom amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".3");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Discount: 10% // Discount: 10%
s.Driver.ElementDoesNotExist(By.Id("CartDiscount")); s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
s.Driver.FindElement(By.Id("Discount")).SendKeys("10"); s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
Assert.Contains("10% = 0,30 €", s.Driver.FindElement(By.Id("CartDiscount")).Text); Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
Assert.Equal("2,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text); Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Tip: 10% // Tip: 10%
s.Driver.ElementDoesNotExist(By.Id("CartTip")); s.Driver.ElementDoesNotExist(By.Id("CartTip"));
s.Driver.FindElement(By.Id("Tip-10")).Click(); s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("10% = 0,27 €", s.Driver.FindElement(By.Id("CartTip")).Text); Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
Assert.Equal("2,97 €", s.Driver.FindElement(By.Id("CartTotal")).Text); Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Pay // Check values on checkout page
s.Driver.FindElement(By.Id("CartSubmit")).Click(); s.Driver.FindElement(By.Id("CartSubmit")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
s.Driver.FindElement(By.Id("DetailsToggle")).Click(); s.Driver.FindElement(By.Id("DetailsToggle")).Click();
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat")); s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
Assert.Contains("2,97 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text); Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
// Pay
s.PayInvoice();
// Check inventory got updated and is now 3 instead of 5
s.Driver.Navigate().GoToUrl(posUrl);
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
} }
[Fact] [Fact]

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -50,45 +51,47 @@ namespace BTCPayServer.HostedServices
} }
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item => }).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
item.Inventory.HasValue && item.Inventory.HasValue &&
updateAppInventory.Items.ContainsKey(item.Id))); updateAppInventory.Items.FirstOrDefault(i => i.Id == item.Id) != null));
foreach (var valueTuple in apps) foreach (var app in apps)
{ {
foreach (var item1 in valueTuple.Items.Where(item => foreach (var cartItem in updateAppInventory.Items)
updateAppInventory.Items.ContainsKey(item.Id)))
{ {
var item = app.Items.FirstOrDefault(item => item.Id == cartItem.Id);
if (item == null) continue;
if (updateAppInventory.Deduct) if (updateAppInventory.Deduct)
{ {
item1.Inventory -= updateAppInventory.Items[item1.Id]; item.Inventory -= cartItem.Count;
} }
else else
{ {
item1.Inventory += updateAppInventory.Items[item1.Id]; item.Inventory += cartItem.Count;
} }
} }
switch (valueTuple.Data.AppType) switch (app.Data.AppType)
{ {
case PointOfSaleAppType.AppType: case PointOfSaleAppType.AppType:
((PointOfSaleSettings)valueTuple.Settings).Template = ((PointOfSaleSettings)app.Settings).Template =
AppService.SerializeTemplate(valueTuple.Items); AppService.SerializeTemplate(app.Items);
break; break;
case CrowdfundAppType.AppType: case CrowdfundAppType.AppType:
((CrowdfundSettings)valueTuple.Settings).PerksTemplate = ((CrowdfundSettings)app.Settings).PerksTemplate =
AppService.SerializeTemplate(valueTuple.Items); AppService.SerializeTemplate(app.Items);
break; break;
default: default:
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
valueTuple.Data.SetSettings(valueTuple.Settings); app.Data.SetSettings(app.Settings);
await _appService.UpdateOrCreateApp(valueTuple.Data); await _appService.UpdateOrCreateApp(app.Data);
} }
} }
else if (evt is InvoiceEvent invoiceEvent) else if (evt is InvoiceEvent invoiceEvent)
{ {
Dictionary<string, int> cartItems = null; List<PosCartItem> cartItems = null;
bool deduct; bool deduct;
switch (invoiceEvent.Name) switch (invoiceEvent.Name)
{ {
@ -104,8 +107,8 @@ namespace BTCPayServer.HostedServices
return; return;
} }
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) || if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems))) AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems))
{ {
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice); var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
@ -114,13 +117,18 @@ namespace BTCPayServer.HostedServices
return; return;
} }
var items = cartItems ?? new Dictionary<string, int>(); var items = cartItems?.ToList() ?? new List<PosCartItem>();
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode)) if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
{ {
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1); items.Add(new PosCartItem
{
Id = invoiceEvent.Invoice.Metadata.ItemCode,
Count = 1,
Price = invoiceEvent.Invoice.Price
});
} }
_eventAggregator.Publish(new UpdateAppInventory() _eventAggregator.Publish(new UpdateAppInventory
{ {
Deduct = deduct, Deduct = deduct,
Items = items, Items = items,
@ -134,7 +142,7 @@ namespace BTCPayServer.HostedServices
public class UpdateAppInventory public class UpdateAppInventory
{ {
public string[] AppId { get; set; } public string[] AppId { get; set; }
public Dictionary<string, int> Items { get; set; } public List<PosCartItem> Items { get; set; }
public bool Deduct { get; set; } public bool Deduct { get; set; }
public override string ToString() public override string ToString()

View file

@ -171,7 +171,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
decimal? price; decimal? price;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null; Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null; ViewPointOfSaleViewModel.Item choice = null;
Dictionary<string, int> cartItems = null; List<PosCartItem> cartItems = null;
ViewPointOfSaleViewModel.Item[] choices = null; ViewPointOfSaleViewModel.Item[] choices = null;
if (!string.IsNullOrEmpty(choiceKey)) if (!string.IsNullOrEmpty(choiceKey))
{ {
@ -208,16 +208,15 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return NotFound(); return NotFound();
title = settings.Title; title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items // if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
price = amount; price = amount;
if (currentView == PosViewType.Cart && if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems))
AppService.TryParsePosCartItems(jposData, out cartItems))
{ {
price = 0.0m; price = 0.0m;
choices = AppService.Parse(settings.Template, false); choices = AppService.Parse(settings.Template, false);
foreach (var cartItem in cartItems) foreach (var cartItem in cartItems)
{ {
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key); var itemChoice = choices.FirstOrDefault(item => item.Id == cartItem.Id);
if (itemChoice == null) if (itemChoice == null)
return NotFound(); return NotFound();
@ -225,20 +224,21 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{ {
switch (itemChoice.Inventory) switch (itemChoice.Inventory)
{ {
case int i when i <= 0: case <= 0:
return RedirectToAction(nameof(ViewPointOfSale), new { appId }); return RedirectToAction(nameof(ViewPointOfSale), new { appId });
case int inventory when inventory < cartItem.Value: case { } inventory when inventory < cartItem.Count:
return RedirectToAction(nameof(ViewPointOfSale), new { appId }); return RedirectToAction(nameof(ViewPointOfSale), new { appId });
} }
} }
decimal expectedCartItemPrice = 0; var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup
if (itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup) ? itemChoice.Price ?? 0
{ : 0;
expectedCartItemPrice = itemChoice.Price ?? 0;
}
price += expectedCartItemPrice * cartItem.Value; if (cartItem.Price < expectedCartItemPrice)
cartItem.Price = expectedCartItemPrice;
price += cartItem.Price * cartItem.Count;
} }
if (customAmount is { } c) if (customAmount is { } c)
price += c; price += c;
@ -315,7 +315,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{ {
Amount = price, Amount = price,
Currency = settings.Currency, Currency = settings.Currency,
Metadata = new InvoiceMetadata() Metadata = new InvoiceMetadata
{ {
ItemCode = choice?.Id, ItemCode = choice?.Id,
ItemDesc = title, ItemDesc = title,
@ -358,17 +358,19 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
receiptData = new JObject(); receiptData = new JObject();
if (cartItems is not null && choices is not null) if (cartItems is not null && choices is not null)
{ {
var selectedChoices = choices.Where(item => cartItems.Keys.Contains(item.Id)) var posCartItems = cartItems.ToList();
var selectedChoices = choices
.Where(item => posCartItems.Any(cartItem => cartItem.Id == item.Id))
.ToDictionary(item => item.Id); .ToDictionary(item => item.Id);
var cartData = new JObject(); var cartData = new JObject();
foreach (KeyValuePair<string, int> cartItem in cartItems) foreach (PosCartItem cartItem in posCartItems)
{ {
if (selectedChoices.TryGetValue(cartItem.Key, out var selectedChoice)) if (!selectedChoices.TryGetValue(cartItem.Id, out var selectedChoice)) continue;
{ var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
cartData.Add(selectedChoice.Title ?? selectedChoice.Id, var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, 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)}")}"); var ident = selectedChoice.Title ?? selectedChoice.Id;
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
} cartData.Add(key, $"{singlePrice} x {cartItem.Count} = {totalPrice}");
} }
receiptData.Add("Cart", cartData); receiptData.Add("Cart", cartData);
} }
@ -621,7 +623,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return View("PointOfSale/UpdatePointOfSale", vm); return View("PointOfSale/UpdatePointOfSale", vm);
} }
var storeBlob = GetCurrentStore().GetStoreBlob();
var settings = new PointOfSaleSettings var settings = new PointOfSaleSettings
{ {
Title = vm.Title, Title = vm.Title,
@ -640,11 +641,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
RedirectUrl = vm.RedirectUrl, RedirectUrl = vm.RedirectUrl,
Description = vm.Description, Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS, EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically) FormId = vm.FormId
}; };
settings.FormId = vm.FormId;
app.Name = vm.AppName; app.Name = vm.AppName;
app.SetSettings(settings); app.SetSettings(settings);
await _appService.UpdateOrCreateApp(app); await _appService.UpdateOrCreateApp(app);

View file

@ -411,7 +411,6 @@ namespace BTCPayServer.Services.Apps
return false; return false;
if (cartObject is null) if (cartObject is null)
return false; return false;
cartItems = new(); cartItems = new();
foreach (var o in cartObject.OfType<JObject>()) foreach (var o in cartObject.OfType<JObject>())
{ {
@ -428,6 +427,29 @@ namespace BTCPayServer.Services.Apps
return true; return true;
} }
public static bool TryParsePosCartItems(JObject? posData, [MaybeNullWhen(false)] out List<PosCartItem> cartItems)
{
cartItems = null;
if (posData is null)
return false;
if (!posData.TryGetValue("cart", out var cartObject))
return false;
cartItems = new List<PosCartItem>();
foreach (var o in cartObject.OfType<JObject>())
{
var id = o.GetValue("id", StringComparison.InvariantCulture)?.ToString();
if (id == null) continue;
var countStr = o.GetValue("count", StringComparison.InvariantCulture)?.ToString() ?? string.Empty;
var price = o.GetValue("price")?.Value<decimal>() ?? 0m;
if (int.TryParse(countStr, out var count))
{
cartItems.Add(new PosCartItem { Id = id, Count = count, Price = price });
}
}
return true;
}
public async Task SetDefaultSettings(AppData appData, string defaultCurrency) public async Task SetDefaultSettings(AppData appData, string defaultCurrency)
{ {
var app = GetAppType(appData.AppType); var app = GetAppType(appData.AppType);
@ -449,6 +471,13 @@ namespace BTCPayServer.Services.Apps
#nullable restore #nullable restore
} }
public class PosCartItem
{
public string Id { get; set; }
public int Count { get; set; }
public decimal Price { get; set; }
}
public class ItemStats public class ItemStats
{ {
public string ItemCode { get; set; } public string ItemCode { get; set; }

View file

@ -83,7 +83,7 @@
} }
@if (item.Inventory.HasValue) @if (item.Inventory.HasValue)
{ {
<span class="badge text-bg-warning" v-text="inventoryText(@index)"> <span class="badge text-bg-warning inventory" v-text="inventoryText(@index)">
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out") @(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
</span> </span>
} }
@ -95,11 +95,18 @@
</div> </div>
@if (inStock) @if (inStock)
{ {
<div class="card-footer bg-transparent border-0 pt-0 pb-3"> <form class="card-footer bg-transparent border-0 pt-0 pb-3">
@if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed)
{
<div class="input-group mb-2">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@item.Price" required v-on:click.stop>
</div>
}
<button type="button" class="btn btn-primary w-100" :disabled="!inStock(@index)"> <button type="button" class="btn btn-primary w-100" :disabled="!inStock(@index)">
@Safe.RawEncode(buttonText) @Safe.RawEncode(buttonText)
</button> </button>
</div> </form>
<div class="posItem-added"><vc:icon symbol="checkmark" /></div> <div class="posItem-added"><vc:icon symbol="checkmark" /></div>
} }
</div> </div>
@ -141,7 +148,7 @@
</td> </td>
<td class="align-middle"> <td class="align-middle">
<div class="d-flex align-items-center gap-2 justify-content-end quantity"> <div class="d-flex align-items-center gap-2 justify-content-end quantity">
<span class="badge text-bg-warning" v-if="item.inventory"> <span class="badge text-bg-warning inventory" v-if="item.inventory">
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }} {{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
</span> </span>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">

View file

@ -121,7 +121,17 @@ document.addEventListener("DOMContentLoaded",function () {
if (!this.inStock(index)) return false; if (!this.inStock(index)) return false;
const item = this.items[index]; const item = this.items[index];
let itemInCart = this.cart.find(lineItem => lineItem.id === item.id); const $posItem = this.$refs.posItems.querySelectorAll('.posItem')[index];
// Check if price is needed
const isFixedPrice = item.priceType.toLowerCase() === 'fixed';
if (!isFixedPrice) {
const $amount = $posItem.querySelector('input[name="amount"]');
if (!$amount.reportValidity()) return false;
item.price = parseFloat($amount.value);
}
let itemInCart = this.cart.find(lineItem => lineItem.id === item.id && lineItem.price === item.price);
// Add new item because it doesn't exist yet // Add new item because it doesn't exist yet
if (!itemInCart) { if (!itemInCart) {
@ -138,7 +148,6 @@ document.addEventListener("DOMContentLoaded",function () {
itemInCart.count += 1; itemInCart.count += 1;
// Animate // Animate
const $posItem = this.$refs.posItems.querySelectorAll('.posItem')[index];
if(!$posItem.classList.contains(POS_ITEM_ADDED_CLASS)) $posItem.classList.add(POS_ITEM_ADDED_CLASS); if(!$posItem.classList.contains(POS_ITEM_ADDED_CLASS)) $posItem.classList.add(POS_ITEM_ADDED_CLASS);
return true; return true;