mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 09:19:24 +01:00
POS: Handle flexible price items in cart view (#5238)
This commit is contained in:
parent
19d360a543
commit
d67ebd957e
7 changed files with 166 additions and 68 deletions
|
@ -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);
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
if (cartItem.Price < expectedCartItemPrice)
|
||||||
|
cartItem.Price = expectedCartItemPrice;
|
||||||
|
|
||||||
price += expectedCartItemPrice * cartItem.Value;
|
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);
|
||||||
|
|
|
@ -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>())
|
||||||
{
|
{
|
||||||
|
@ -427,6 +426,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)
|
||||||
{
|
{
|
||||||
|
@ -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; }
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue